mihomo-cli 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2917,7 +2917,8 @@ function dump(input, options = {}) {
2917
2917
  }
2918
2918
 
2919
2919
  // src/constants.ts
2920
- var AVAILABLE_MIRRORS = ["gh-proxy.org", "v6.gh-proxy.org", "hk.gh-proxy.org", "cdn.gh-proxy.org"];
2920
+ var AVAILABLE_MIRRORS = ["v6.gh-proxy.org", "gh-proxy.org", "hk.gh-proxy.org", "cdn.gh-proxy.org"];
2921
+ var DEFAULT_MIRROR = "https://v6.gh-proxy.org/";
2921
2922
  var UI_URLS = {
2922
2923
  zash: "https://board.zash.run.place",
2923
2924
  dash: "https://metacubex.github.io/metacubexd",
@@ -2974,6 +2975,7 @@ var DEFAULT_TEST_URL = "http://www.gstatic.com/generate_204";
2974
2975
  var DEFAULT_CLEAN_ROUNDS = 2;
2975
2976
  var AUTO_CLEAN_THRESHOLD = 100;
2976
2977
  var AUTO_CLEAN_THRESHOLD_GITHUB = 50;
2978
+ var AUTO_CLEAN_COOLDOWN_HOURS = 12;
2977
2979
 
2978
2980
  // src/overwrite.ts
2979
2981
  import fs3 from "fs";
@@ -3097,6 +3099,7 @@ function maskUrl(url) {
3097
3099
  }
3098
3100
  if (parsed.username) parsed.username = "***";
3099
3101
  if (parsed.password) parsed.password = "***";
3102
+ parsed.pathname = parsed.pathname.split("/").map((seg) => seg.length >= 16 ? `${seg.slice(0, 4)}***${seg.slice(-4)}` : seg).join("/");
3100
3103
  return parsed.toString();
3101
3104
  } catch {
3102
3105
  if (url.length > 30) {
@@ -3112,6 +3115,12 @@ function readSubscriptionCache() {
3112
3115
  const content = fs2.readFileSync(PATHS.subscriptionsCacheFile, "utf8");
3113
3116
  return JSON.parse(content);
3114
3117
  } catch {
3118
+ try {
3119
+ fs2.copyFileSync(PATHS.subscriptionsCacheFile, `${PATHS.subscriptionsCacheFile}.bak`);
3120
+ console.warn(`\u8B66\u544A: \u8BA2\u9605\u7F13\u5B58\u683C\u5F0F\u635F\u574F\uFF0C\u5DF2\u5907\u4EFD\u5230 ${PATHS.subscriptionsCacheFile}.bak`);
3121
+ } catch {
3122
+ console.warn("\u8B66\u544A: \u8BA2\u9605\u7F13\u5B58\u683C\u5F0F\u635F\u574F\uFF0C\u5DF2\u5FFD\u7565");
3123
+ }
3115
3124
  return {};
3116
3125
  }
3117
3126
  }
@@ -3214,6 +3223,7 @@ function parseOverrideKey(key) {
3214
3223
  let forceOverwrite = false;
3215
3224
  let arrayPrepend = false;
3216
3225
  let arrayAppend = false;
3226
+ let arrayMergeByName = false;
3217
3227
  const lastChar = key[key.length - 1];
3218
3228
  const openAngleCount = (key.match(/</g) || []).length;
3219
3229
  const closeAngleCount = (key.match(/>/g) || []).length;
@@ -3234,6 +3244,9 @@ function parseOverrideKey(key) {
3234
3244
  } else {
3235
3245
  actualKey = unwrapped;
3236
3246
  }
3247
+ } else if (actualKey.startsWith("~")) {
3248
+ arrayMergeByName = true;
3249
+ actualKey = actualKey.slice(1);
3237
3250
  } else {
3238
3251
  if (actualKey.startsWith("+")) {
3239
3252
  arrayPrepend = true;
@@ -3244,7 +3257,7 @@ function parseOverrideKey(key) {
3244
3257
  actualKey = actualKey.slice(0, -1);
3245
3258
  }
3246
3259
  }
3247
- return { key: actualKey, forceOverwrite, arrayPrepend, arrayAppend };
3260
+ return { key: actualKey, forceOverwrite, arrayPrepend, arrayAppend, arrayMergeByName };
3248
3261
  }
3249
3262
  function deepMergeWithOverrides(target, override) {
3250
3263
  let t = target;
@@ -3262,8 +3275,24 @@ function deepMergeWithOverrides(target, override) {
3262
3275
  }
3263
3276
  const result = { ...t };
3264
3277
  for (const [rawKey, value] of Object.entries(override)) {
3265
- const { key, forceOverwrite, arrayPrepend, arrayAppend } = parseOverrideKey(rawKey);
3278
+ const { key, forceOverwrite, arrayPrepend, arrayAppend, arrayMergeByName } = parseOverrideKey(rawKey);
3266
3279
  const existingValue = result[key];
3280
+ if (arrayMergeByName) {
3281
+ const existingArr = Array.isArray(existingValue) ? existingValue : [];
3282
+ const overrideArr = Array.isArray(value) ? value : [value];
3283
+ const merged = [...existingArr];
3284
+ for (const item of overrideArr) {
3285
+ const name = item && typeof item === "object" && !Array.isArray(item) ? item.name : void 0;
3286
+ const idx = name != null ? merged.findIndex((e) => e && typeof e === "object" && e.name === name) : -1;
3287
+ if (idx >= 0) {
3288
+ merged[idx] = deepMergeWithOverrides(merged[idx], item);
3289
+ } else {
3290
+ merged.push(item);
3291
+ }
3292
+ }
3293
+ result[key] = merged;
3294
+ continue;
3295
+ }
3267
3296
  if (arrayPrepend || arrayAppend) {
3268
3297
  const existingArr = Array.isArray(existingValue) ? existingValue : [];
3269
3298
  const overrideArr = Array.isArray(value) ? value : [value];
@@ -3296,6 +3325,69 @@ function setOverwriteEnabled(enabled) {
3296
3325
  function isOverwriteFilename(filename) {
3297
3326
  return filename === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(filename);
3298
3327
  }
3328
+ var MATCH_KEYS = /* @__PURE__ */ new Set(["subscription", "url-domain"]);
3329
+ function normalizeMatch(raw, fileName) {
3330
+ if (raw == null) return void 0;
3331
+ if (typeof raw !== "object" || Array.isArray(raw)) {
3332
+ console.warn(`\u8B66\u544A: \u8986\u5199\u6587\u4EF6 "${fileName}" \u7684 match \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565\u4F5C\u7528\u57DF\u9650\u5B9A`);
3333
+ return void 0;
3334
+ }
3335
+ const result = {};
3336
+ let hasValid = false;
3337
+ for (const [key, value] of Object.entries(raw)) {
3338
+ if (!MATCH_KEYS.has(key)) {
3339
+ console.warn(`\u8B66\u544A: \u8986\u5199\u6587\u4EF6 "${fileName}" \u7684 match \u542B\u672A\u77E5\u952E "${key}"\uFF0C\u5DF2\u5FFD\u7565`);
3340
+ continue;
3341
+ }
3342
+ const arr = (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v.length > 0);
3343
+ if (arr.length === 0) continue;
3344
+ result[key] = arr;
3345
+ hasValid = true;
3346
+ }
3347
+ return hasValid ? result : void 0;
3348
+ }
3349
+ function summarizeMatch(match) {
3350
+ if (!match) return void 0;
3351
+ const parts = [];
3352
+ for (const [key, value] of Object.entries(match)) {
3353
+ const vals = Array.isArray(value) ? value : [value];
3354
+ parts.push(`${key}=${vals.join("/")}`);
3355
+ }
3356
+ return parts.length > 0 ? parts.join(", ") : void 0;
3357
+ }
3358
+ function splitUrlsLocal(url) {
3359
+ return url.split(",").map((u) => u.trim()).filter(Boolean);
3360
+ }
3361
+ function hostMatchesDomain(host, domain) {
3362
+ const h = host.toLowerCase();
3363
+ const d = domain.toLowerCase();
3364
+ return h === d || h.endsWith(`.${d}`);
3365
+ }
3366
+ function matchesScope(match, scope) {
3367
+ if (!match) return true;
3368
+ if (match.subscription) {
3369
+ const names = Array.isArray(match.subscription) ? match.subscription : [match.subscription];
3370
+ if (!scope?.subName || !names.includes(scope.subName)) return false;
3371
+ }
3372
+ if (match["url-domain"]) {
3373
+ const domains = Array.isArray(match["url-domain"]) ? match["url-domain"] : [match["url-domain"]];
3374
+ if (!scope?.subUrl) return false;
3375
+ const hosts = [];
3376
+ for (const u of splitUrlsLocal(scope.subUrl)) {
3377
+ try {
3378
+ hosts.push(new URL(u).hostname);
3379
+ } catch {
3380
+ }
3381
+ }
3382
+ if (hosts.length === 0) return false;
3383
+ const ok = domains.some((d) => hosts.some((h) => hostMatchesDomain(h, d)));
3384
+ if (!ok) return false;
3385
+ }
3386
+ return true;
3387
+ }
3388
+ function filterOverwriteFilesByScope(files, scope) {
3389
+ return files.filter((f) => matchesScope(f.match, scope));
3390
+ }
3299
3391
  function loadOverwriteFile() {
3300
3392
  const dir = USER_DATA_DIR;
3301
3393
  if (!fs3.existsSync(dir)) return [];
@@ -3310,8 +3402,11 @@ function loadOverwriteFile() {
3310
3402
  try {
3311
3403
  const content = fs3.readFileSync(filePath, "utf8");
3312
3404
  const parsed = load(content);
3313
- if (parsed && typeof parsed === "object") {
3314
- results.push({ name: file, path: filePath, config: parsed });
3405
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3406
+ const { match, ...config } = parsed;
3407
+ results.push({ name: file, path: filePath, config, match: normalizeMatch(match, file) });
3408
+ } else if (parsed !== null) {
3409
+ console.warn(`\u8B66\u544A: \u8986\u5199\u6587\u4EF6 "${file}" \u9876\u5C42\u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5DF2\u8DF3\u8FC7`);
3315
3410
  }
3316
3411
  } catch (e) {
3317
3412
  console.warn(`\u8B66\u544A: \u8986\u5199\u6587\u4EF6 "${file}" \u89E3\u6790\u5931\u8D25: ${e.message}`);
@@ -3320,9 +3415,9 @@ function loadOverwriteFile() {
3320
3415
  return results;
3321
3416
  }
3322
3417
  function applyOverwrite(baseConfig, preloadedFiles) {
3323
- if (!isOverwriteEnabled()) return baseConfig;
3418
+ if (!isOverwriteEnabled()) return { ...baseConfig };
3324
3419
  const overwriteFiles = preloadedFiles || loadOverwriteFile();
3325
- if (overwriteFiles.length === 0) return baseConfig;
3420
+ if (overwriteFiles.length === 0) return { ...baseConfig };
3326
3421
  let result = { ...baseConfig };
3327
3422
  for (const file of overwriteFiles) {
3328
3423
  result = deepMergeWithOverrides(result, file.config);
@@ -3338,7 +3433,8 @@ function listOverwriteFile() {
3338
3433
  files: files.map((f) => ({
3339
3434
  name: f.name,
3340
3435
  path: f.path,
3341
- keys: Object.keys(f.config || {})
3436
+ keys: Object.keys(f.config || {}),
3437
+ scope: summarizeMatch(f.match)
3342
3438
  }))
3343
3439
  };
3344
3440
  }
@@ -3349,6 +3445,7 @@ import { createRequire } from "module";
3349
3445
  var require2 = createRequire(import.meta.url);
3350
3446
  var pkg = require2("../package.json");
3351
3447
  var VERSION = pkg.version;
3448
+ var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
3352
3449
  var sleepBuf = new Int32Array(new SharedArrayBuffer(4));
3353
3450
  var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
3354
3451
  function colorize(code, str) {
@@ -3408,6 +3505,7 @@ function formatBytes(bytes) {
3408
3505
  }
3409
3506
  function formatTimestamp(ts) {
3410
3507
  if (ts === void 0 || ts === null) return "\u672A\u77E5";
3508
+ if (ts === 0) return "\u6C38\u4E45";
3411
3509
  try {
3412
3510
  return new Date(ts * 1e3).toLocaleString("zh-CN");
3413
3511
  } catch {
@@ -3429,7 +3527,7 @@ function formatDate(dateOrIso) {
3429
3527
  }
3430
3528
  }
3431
3529
  function hasFlag(args, short, long) {
3432
- return !!args && (args.includes(short) || args.includes(long));
3530
+ return !!args && (args.includes(short) || long !== void 0 && args.includes(long));
3433
3531
  }
3434
3532
  function parseIntArg(args, short, long, defaultValue) {
3435
3533
  if (!args) return defaultValue;
@@ -3439,11 +3537,29 @@ function parseIntArg(args, short, long, defaultValue) {
3439
3537
  const val = parseInt(args[i + 1], 10);
3440
3538
  return Number.isNaN(val) ? defaultValue : val;
3441
3539
  }
3540
+ } else if (args[i].startsWith(`${long}=`)) {
3541
+ const val = parseInt(args[i].slice(long.length + 1), 10);
3542
+ if (!Number.isNaN(val)) return val;
3442
3543
  }
3443
3544
  }
3444
3545
  return defaultValue;
3445
3546
  }
3446
3547
  var VALUE_FLAGS = /* @__PURE__ */ new Set(["-t", "--timeout", "-j", "--concurrency", "-r", "--rounds", "-n", "--lines", "-u", "--update-timeout"]);
3548
+ function extractStartOptions(args) {
3549
+ if (!args) return [];
3550
+ const BOOL_FLAGS = /* @__PURE__ */ new Set(["-s", "--no-update", "--no-clean"]);
3551
+ const out = [];
3552
+ for (let i = 0; i < args.length; i++) {
3553
+ const a = args[i];
3554
+ if (VALUE_FLAGS.has(a)) {
3555
+ out.push(a);
3556
+ if (i + 1 < args.length) out.push(args[++i]);
3557
+ } else if (BOOL_FLAGS.has(a) || /^--(timeout|concurrency|rounds|update-timeout)=/.test(a)) {
3558
+ out.push(a);
3559
+ }
3560
+ }
3561
+ return out;
3562
+ }
3447
3563
  function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
3448
3564
  if (!args) return null;
3449
3565
  for (let i = startIdx; i < args.length; i++) {
@@ -3495,7 +3611,12 @@ function createHttpClient(options = {}) {
3495
3611
  }
3496
3612
  throw error;
3497
3613
  }
3498
- const data = config?.responseType === "json" ? await response.json() : await response.text();
3614
+ const declaredLen = Number(response.headers.get("content-length"));
3615
+ if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
3616
+ throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
3617
+ }
3618
+ const text = await readBodyWithLimit(response, controller);
3619
+ const data = config?.responseType === "json" ? JSON.parse(text) : text;
3499
3620
  return { data, headers: response.headers, status: response.status };
3500
3621
  } finally {
3501
3622
  clearTimeout(timer);
@@ -3503,6 +3624,29 @@ function createHttpClient(options = {}) {
3503
3624
  }
3504
3625
  };
3505
3626
  }
3627
+ async function readBodyWithLimit(response, controller) {
3628
+ if (!response.body) return response.text();
3629
+ const reader = response.body.getReader();
3630
+ const chunks = [];
3631
+ let total = 0;
3632
+ try {
3633
+ while (true) {
3634
+ const { done, value } = await reader.read();
3635
+ if (done) break;
3636
+ if (value) {
3637
+ total += value.byteLength;
3638
+ if (total > MAX_RESPONSE_BYTES) {
3639
+ controller.abort();
3640
+ throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
3641
+ }
3642
+ chunks.push(value);
3643
+ }
3644
+ }
3645
+ } finally {
3646
+ reader.releaseLock();
3647
+ }
3648
+ return Buffer.concat(chunks).toString("utf8");
3649
+ }
3506
3650
  function normalizeMirrorUrl(val) {
3507
3651
  if (!val) return null;
3508
3652
  if (val === "direct" || val === "no" || val === "none") return null;
@@ -3522,19 +3666,23 @@ function parseMirrorArg(args) {
3522
3666
  if (args.includes("--no-mirror") || args.includes("--direct")) {
3523
3667
  return { mirror: null, isOverride: true, type: "download" };
3524
3668
  }
3669
+ const mirrorAllEq = args.find((a) => a.startsWith("--mirror-all="));
3525
3670
  const mirrorAllIdx = args.indexOf("--mirror-all");
3526
- if (mirrorAllIdx >= 0) {
3527
- const nextArg = args[mirrorAllIdx + 1];
3671
+ if (mirrorAllIdx >= 0 || mirrorAllEq) {
3672
+ const inline = mirrorAllEq?.slice("--mirror-all=".length);
3673
+ const nextArg = inline ?? args[mirrorAllIdx + 1];
3528
3674
  if (!nextArg || nextArg.startsWith("-")) {
3529
- return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "all" };
3675
+ return { mirror: DEFAULT_MIRROR, isOverride: true, type: "all" };
3530
3676
  }
3531
3677
  return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "all" };
3532
3678
  }
3679
+ const mirrorEq = args.find((a) => a.startsWith("--mirror="));
3533
3680
  const mirrorIdx = args.indexOf("--mirror");
3534
- if (mirrorIdx >= 0) {
3535
- const nextArg = args[mirrorIdx + 1];
3681
+ if (mirrorIdx >= 0 || mirrorEq) {
3682
+ const inline = mirrorEq?.slice("--mirror=".length);
3683
+ const nextArg = inline ?? args[mirrorIdx + 1];
3536
3684
  if (!nextArg || nextArg.startsWith("-")) {
3537
- return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "download" };
3685
+ return { mirror: DEFAULT_MIRROR, isOverride: true, type: "download" };
3538
3686
  }
3539
3687
  return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "download" };
3540
3688
  }
@@ -3576,7 +3724,8 @@ function collectOverwriteProxyNames(overwriteFiles) {
3576
3724
  if ((key === "+proxies" || key === "proxies+") && Array.isArray(value)) {
3577
3725
  for (const proxy of value) {
3578
3726
  if (proxy && typeof proxy === "object" && "name" in proxy) {
3579
- names.push(proxy.name);
3727
+ const name = proxy.name;
3728
+ if (typeof name === "string" && name.length > 0) names.push(name);
3580
3729
  }
3581
3730
  }
3582
3731
  }
@@ -3614,6 +3763,7 @@ function deduplicateByName(items) {
3614
3763
  });
3615
3764
  return { result, names, duplicates };
3616
3765
  }
3766
+ var NON_TARGET_RULE_TYPES = /* @__PURE__ */ new Set(["SUB-RULE"]);
3617
3767
  function getRuleTarget(rule) {
3618
3768
  const parts = rule.split(",");
3619
3769
  if (parts.length < 2) return "";
@@ -3667,6 +3817,8 @@ function validateConfig(config) {
3667
3817
  if (rules.length > 0) {
3668
3818
  const removedRules = [];
3669
3819
  config.rules = rules.filter((rule) => {
3820
+ const ruleType = rule.split(",")[0]?.trim().toUpperCase();
3821
+ if (NON_TARGET_RULE_TYPES.has(ruleType)) return true;
3670
3822
  const target = getRuleTarget(rule);
3671
3823
  if (!target || validNames.has(target)) return true;
3672
3824
  removedRules.push(rule);
@@ -3678,13 +3830,14 @@ function validateConfig(config) {
3678
3830
  }
3679
3831
  return warnings;
3680
3832
  }
3681
- function buildConfig(subRawContent, mode) {
3833
+ function buildConfig(subRawContent, mode, scope) {
3682
3834
  const subscriptionConfig = parseYamlOrJson(subRawContent, "\u8BA2\u9605\u5185\u5BB9");
3683
3835
  if (!subscriptionConfig) {
3684
3836
  throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
3685
3837
  }
3686
3838
  const overwriteEnabled = isOverwriteEnabled();
3687
- const overwriteFiles = overwriteEnabled ? loadOverwriteFile() : [];
3839
+ const allFiles = overwriteEnabled ? loadOverwriteFile() : [];
3840
+ const overwriteFiles = filterOverwriteFilesByScope(allFiles, scope);
3688
3841
  const withOverwrites = applyOverwrite(subscriptionConfig, overwriteFiles);
3689
3842
  if (overwriteFiles.length > 0) {
3690
3843
  excludeOverwriteProxiesFromIncludeAll(withOverwrites, overwriteFiles);
@@ -3695,7 +3848,6 @@ function buildConfig(subRawContent, mode) {
3695
3848
  systemConfig[key] = value;
3696
3849
  }
3697
3850
  }
3698
- systemConfig["allow-lan"] = false;
3699
3851
  systemConfig["external-controller"] = BASE_CONFIG["external-controller"];
3700
3852
  systemConfig["mixed-port"] = BASE_CONFIG["mixed-port"];
3701
3853
  delete withOverwrites["mixed-port"];
@@ -3704,6 +3856,11 @@ function buildConfig(subRawContent, mode) {
3704
3856
  delete withOverwrites["external-ui"];
3705
3857
  delete withOverwrites["external-ui-name"];
3706
3858
  delete withOverwrites["external-ui-url"];
3859
+ delete withOverwrites.secret;
3860
+ const controllerSecret = readSettings().controller_secret;
3861
+ if (controllerSecret) {
3862
+ systemConfig.secret = controllerSecret;
3863
+ }
3707
3864
  if (mode === "tun") {
3708
3865
  systemConfig.tun = TUN_CONFIG.tun;
3709
3866
  const subDns = withOverwrites.dns || {};
@@ -3892,6 +4049,33 @@ import path5 from "path";
3892
4049
  import { spawn, spawnSync as spawnSync3 } from "child_process";
3893
4050
  import fs5 from "fs";
3894
4051
  import path4 from "path";
4052
+
4053
+ // src/lifecycle.ts
4054
+ var cleanupFns = /* @__PURE__ */ new Set();
4055
+ var silentSigint = false;
4056
+ function setSilentSigint(value) {
4057
+ silentSigint = value;
4058
+ }
4059
+ function isSilentSigint() {
4060
+ return silentSigint;
4061
+ }
4062
+ function registerCleanup(fn) {
4063
+ cleanupFns.add(fn);
4064
+ return () => {
4065
+ cleanupFns.delete(fn);
4066
+ };
4067
+ }
4068
+ function runCleanup() {
4069
+ for (const fn of cleanupFns) {
4070
+ try {
4071
+ fn();
4072
+ } catch {
4073
+ }
4074
+ }
4075
+ cleanupFns.clear();
4076
+ }
4077
+
4078
+ // src/process.ts
3895
4079
  var PROCESS_WAIT_ATTEMPTS = 50;
3896
4080
  var PROCESS_WAIT_INTERVAL = 100;
3897
4081
  var STARTUP_WAIT_MS = 800;
@@ -3950,6 +4134,9 @@ function checkStaleState() {
3950
4134
  needsSudo: hasRootProcess || hasRootPidFile
3951
4135
  };
3952
4136
  }
4137
+ function hasRootResidue() {
4138
+ return checkStaleState().needsSudo;
4139
+ }
3953
4140
  function savePid(pid) {
3954
4141
  ensureDirs();
3955
4142
  fs5.writeFileSync(PATHS.pidFile, pid.toString(), { mode: 384 });
@@ -3968,23 +4155,10 @@ function clearPid() {
3968
4155
  }
3969
4156
  }
3970
4157
  }
3971
- function killProcess(pid, needsSudo = false) {
4158
+ function killProcess(pid) {
3972
4159
  try {
3973
- if (needsSudo) {
3974
- const result = spawnSync3("sudo", ["kill", "-9", String(pid)], { stdio: "inherit", timeout: 1e4 });
3975
- if (result.status === 0) {
3976
- return true;
3977
- }
3978
- try {
3979
- process.kill(pid, "SIGKILL");
3980
- return true;
3981
- } catch {
3982
- return false;
3983
- }
3984
- } else {
3985
- process.kill(pid, "SIGKILL");
3986
- return true;
3987
- }
4160
+ process.kill(pid, "SIGKILL");
4161
+ return true;
3988
4162
  } catch {
3989
4163
  return false;
3990
4164
  }
@@ -4030,7 +4204,7 @@ function cleanupAll(forceSudo = false) {
4030
4204
  killedCount = pids.length;
4031
4205
  } else {
4032
4206
  for (const pid of pids) {
4033
- if (killProcess(pid, false)) {
4207
+ if (killProcess(pid)) {
4034
4208
  killedCount++;
4035
4209
  } else {
4036
4210
  failedPids.push(pid);
@@ -4082,12 +4256,13 @@ for i in 1 2 3 4 5; do
4082
4256
  fi
4083
4257
  done
4084
4258
 
4085
- # \u5931\u8D25\uFF0C\u663E\u793A\u65E5\u5FD7
4259
+ # \u5931\u8D25\uFF0C\u663E\u793A\u65E5\u5FD7\uFF08\u9000\u51FA\u7801 2\uFF1A\u907F\u5F00 sudo \u7684 1=\u9274\u6743\u5931\u8D25/\u53D6\u6D88\uFF0C\u4F9B\u8C03\u7528\u65B9\u533A\u5206\uFF09
4260
+ rm -f "\${PID_FILE}" 2>/dev/null || true
4086
4261
  echo "TUN \u542F\u52A8\u5931\u8D25"
4087
4262
  echo ""
4088
4263
  echo "--- \u65E5\u5FD7 ---"
4089
4264
  tail -25 "\${LOG_FILE}" 2>/dev/null
4090
- exit 1
4265
+ exit 2
4091
4266
  `;
4092
4267
  const scriptPath = path4.join(DIRS.runtime, "launch-tun.sh");
4093
4268
  fs5.writeFileSync(scriptPath, scriptContent, { mode: 448 });
@@ -4145,7 +4320,7 @@ async function startMixedMode(staleState) {
4145
4320
  if (staleState.needsCleanup) {
4146
4321
  if (staleState.needsSudo) {
4147
4322
  console.log("\n\u53D1\u73B0\u9700\u8981 root \u6743\u9650\u6E05\u7406\u7684\u6B8B\u7559\u8FDB\u7A0B/\u6587\u4EF6");
4148
- console.log("\u8BF7\u5148\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo");
4323
+ console.log(`\u8BF7\u5148\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo && sudo rm -f ${PATHS.pidFile}`);
4149
4324
  console.log("\u6216\u8005\u5207\u6362\u5230 TUN \u6A21\u5F0F\uFF0C\u542F\u52A8\u65F6\u4F1A\u81EA\u52A8\u6E05\u7406");
4150
4325
  throw new Error("\u5B58\u5728\u9700\u8981 root \u6743\u9650\u6E05\u7406\u7684\u6B8B\u7559");
4151
4326
  }
@@ -4176,9 +4351,15 @@ async function startMixedMode(staleState) {
4176
4351
  detached: true,
4177
4352
  stdio: ["ignore", logFd, logFd]
4178
4353
  });
4354
+ child.on("error", () => {
4355
+ });
4179
4356
  fs5.closeSync(logFd);
4180
4357
  child.unref();
4181
4358
  const pid = child.pid;
4359
+ if (!pid) {
4360
+ clearPid();
4361
+ throw new Error("\u542F\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u521B\u5EFA\u5185\u6838\u8FDB\u7A0B\uFF08\u5185\u6838\u4E8C\u8FDB\u5236\u53EF\u80FD\u4E0D\u53EF\u6267\u884C\uFF09");
4362
+ }
4182
4363
  savePid(pid);
4183
4364
  await new Promise((resolve) => setTimeout(resolve, STARTUP_WAIT_MS));
4184
4365
  if (!isRunning()) {
@@ -4219,6 +4400,9 @@ async function startTunMode(staleState) {
4219
4400
  if (e.status === 1) {
4220
4401
  throw new Error("\u5BC6\u7801\u9519\u8BEF\u6216\u53D6\u6D88");
4221
4402
  }
4403
+ if (e.status === 2) {
4404
+ throw new Error("TUN \u542F\u52A8\u5931\u8D25\uFF08\u8BE6\u89C1\u4E0A\u65B9\u65E5\u5FD7\uFF09");
4405
+ }
4222
4406
  throw new Error(e.message);
4223
4407
  }
4224
4408
  try {
@@ -4383,6 +4567,7 @@ function viewLogWithTail(logPath, options) {
4383
4567
  tailArgs.push("-n", lines.toString());
4384
4568
  tailArgs.push(logPath);
4385
4569
  const tail = spawn("tail", tailArgs, { stdio: "inherit" });
4570
+ if (follow) setSilentSigint(true);
4386
4571
  tail.on("close", () => process.exit(0));
4387
4572
  tail.on("error", (e) => {
4388
4573
  console.error(`\u65E0\u6CD5\u8BFB\u53D6\u65E5\u5FD7: ${e.message}`);
@@ -4581,7 +4766,11 @@ async function restartDaemon() {
4581
4766
 
4582
4767
  // src/subscription.ts
4583
4768
  function isGithubUrl(url) {
4584
- return /github\.com|raw\.githubusercontent\.com/i.test(url);
4769
+ const githubRe = /github\.com|raw\.githubusercontent\.com/i;
4770
+ if (isMultiUrl(url)) {
4771
+ return splitUrls(url).every((u) => githubRe.test(u));
4772
+ }
4773
+ return githubRe.test(url);
4585
4774
  }
4586
4775
  function getDefaultUpdateInterval(url) {
4587
4776
  return isGithubUrl(url) ? DEFAULT_UPDATE_INTERVAL_HOURS_GITHUB : DEFAULT_UPDATE_INTERVAL_HOURS;
@@ -4593,6 +4782,14 @@ var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4593
4782
  function isMultiUrl(url) {
4594
4783
  return url.includes(",");
4595
4784
  }
4785
+ function isValidHttpUrl(url) {
4786
+ try {
4787
+ const u = new URL(url.trim());
4788
+ return u.protocol === "http:" || u.protocol === "https:";
4789
+ } catch {
4790
+ return false;
4791
+ }
4792
+ }
4596
4793
  function splitUrls(url) {
4597
4794
  return url.split(",").map((u) => u.trim()).filter(Boolean);
4598
4795
  }
@@ -4708,7 +4905,7 @@ function pickSingleSubscription(subs, pattern) {
4708
4905
  for (const s of subs) console.log(` ${s.name}`);
4709
4906
  process.exit(1);
4710
4907
  }
4711
- async function downloadSubscription(url, subName = "default", signal) {
4908
+ async function downloadSubscription(url, subName = "default", signal, persist = true) {
4712
4909
  let response;
4713
4910
  try {
4714
4911
  response = await HTTP_CLIENT.get(url, { responseType: "text", signal });
@@ -4729,9 +4926,13 @@ async function downloadSubscription(url, subName = "default", signal) {
4729
4926
  }
4730
4927
  const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
4731
4928
  if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4732
- saveSubscriptionRawConfig(subName, content);
4929
+ if (persist) {
4930
+ saveSubscriptionRawConfig(subName, content);
4931
+ }
4733
4932
  const meta = extractSubscriptionMeta(response.headers);
4734
- saveSubscriptionMeta(subName, meta);
4933
+ if (persist) {
4934
+ saveSubscriptionMeta(subName, meta);
4935
+ }
4735
4936
  const proxies = parsed.proxies;
4736
4937
  const proxyGroups = parsed["proxy-groups"];
4737
4938
  return {
@@ -4743,13 +4944,16 @@ async function downloadSubscription(url, subName = "default", signal) {
4743
4944
  username: meta.username
4744
4945
  };
4745
4946
  }
4746
- async function downloadMergedSubscription(urls, subName, signal) {
4947
+ async function downloadMergedSubscription(urls, subName, signal, persist = true) {
4948
+ const internal = new AbortController();
4949
+ const combinedSignal = signal ? AbortSignal.any([signal, internal.signal]) : internal.signal;
4747
4950
  const responses = await Promise.all(
4748
4951
  urls.map(async (url, index) => {
4749
4952
  try {
4750
- const response = await HTTP_CLIENT.get(url, { responseType: "text", signal });
4953
+ const response = await HTTP_CLIENT.get(url, { responseType: "text", signal: combinedSignal });
4751
4954
  return { url, index, response, error: null };
4752
4955
  } catch (e) {
4956
+ internal.abort();
4753
4957
  return { url, index, response: null, error: e };
4754
4958
  }
4755
4959
  })
@@ -4780,9 +4984,13 @@ async function downloadMergedSubscription(urls, subName, signal) {
4780
4984
  }
4781
4985
  base.proxies = baseProxies;
4782
4986
  const mergedContent = dumpYaml(base);
4783
- saveSubscriptionRawConfig(subName, mergedContent);
4987
+ if (persist) {
4988
+ saveSubscriptionRawConfig(subName, mergedContent);
4989
+ }
4784
4990
  const meta = extractSubscriptionMeta(responses[0].response?.headers);
4785
- saveSubscriptionMeta(subName, meta);
4991
+ if (persist) {
4992
+ saveSubscriptionMeta(subName, meta);
4993
+ }
4786
4994
  const proxyGroups = base["proxy-groups"];
4787
4995
  return {
4788
4996
  proxies: baseProxies.length,
@@ -4798,7 +5006,8 @@ function prepareConfigForStart(mode, subName = "default") {
4798
5006
  if (!rawContent) {
4799
5007
  throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
4800
5008
  }
4801
- const buildResult = buildConfig(rawContent, mode);
5009
+ const subUrl = getSubscriptions().find((s) => s.name === subName)?.url;
5010
+ const buildResult = buildConfig(rawContent, mode, { subName, subUrl });
4802
5011
  if (buildResult.warnings.length > 0) {
4803
5012
  for (const warning of buildResult.warnings) {
4804
5013
  console.log(`${colors.yellow("\u81EA\u52A8\u4FEE\u590D:")} ${warning}`);
@@ -4857,16 +5066,23 @@ async function autoUpdateStaleSubscription(options = {}) {
4857
5066
  }
4858
5067
  const timeoutMs = options.timeout ?? DEFAULT_AUTO_UPDATE_TIMEOUT;
4859
5068
  const controller = new AbortController();
4860
- let results;
5069
+ const results = [];
5070
+ const updatePromise = Promise.all(
5071
+ staleSubs.map(
5072
+ (sub) => tryUpdateOne(sub, controller.signal).then((r) => {
5073
+ results.push(r);
5074
+ return r;
5075
+ })
5076
+ )
5077
+ );
4861
5078
  try {
4862
- results = await withTimeout(Promise.all(staleSubs.map((sub) => tryUpdateOne(sub, controller.signal))), timeoutMs);
5079
+ await withTimeout(updatePromise, timeoutMs);
4863
5080
  } catch (e) {
4864
- if (e instanceof TimeoutError) {
4865
- controller.abort();
4866
- console.log(colors.yellow(`\u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 (${timeoutMs / 1e3}s)\uFF0C\u8DF3\u8FC7\u66F4\u65B0\uFF0C\u4F7F\u7528\u7F13\u5B58\u914D\u7F6E`));
4867
- return { total: staleSubs.length, updated: 0, failed: staleSubs.length };
4868
- }
4869
- throw e;
5081
+ if (!(e instanceof TimeoutError)) throw e;
5082
+ controller.abort();
5083
+ await updatePromise.catch(() => {
5084
+ });
5085
+ console.log(colors.yellow(`\u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 (${timeoutMs / 1e3}s)\uFF0C\u5DF2\u5B8C\u6210\u7684\u66F4\u65B0\u751F\u6548\uFF0C\u5176\u4F59\u4F7F\u7528\u7F13\u5B58\u914D\u7F6E`));
4870
5086
  }
4871
5087
  let updatedCount = 0;
4872
5088
  for (const r of results) {
@@ -4949,6 +5165,19 @@ function normalizeProxyNamesBeforeSave(parsed) {
4949
5165
  group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
4950
5166
  }
4951
5167
  }
5168
+ const rules = parsed.raw.rules;
5169
+ if (Array.isArray(rules)) {
5170
+ parsed.raw.rules = rules.map((rule) => {
5171
+ if (typeof rule !== "string") return rule;
5172
+ const parts = rule.split(",");
5173
+ if (parts.length < 2) return rule;
5174
+ const targetIdx = parts[parts.length - 1].trim().toLowerCase() === "no-resolve" && parts.length >= 3 ? parts.length - 2 : parts.length - 1;
5175
+ const target = parts[targetIdx].trim();
5176
+ const renamed = renameMap.get(target);
5177
+ if (renamed) parts[targetIdx] = renamed;
5178
+ return parts.join(",");
5179
+ });
5180
+ }
4952
5181
  return renameMap.size;
4953
5182
  }
4954
5183
  function cleanDeadProxies(parsed, deadNames) {
@@ -4978,11 +5207,14 @@ function cleanDeadProxies(parsed, deadNames) {
4978
5207
  group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
4979
5208
  }
4980
5209
  }
5210
+ }
5211
+ const removedTargets = /* @__PURE__ */ new Set([...removedGroupNames, ...deadNames]);
5212
+ if (removedTargets.size > 0) {
4981
5213
  const rules = parsed.raw.rules;
4982
5214
  if (Array.isArray(rules)) {
4983
5215
  parsed.raw.rules = rules.filter((rule) => {
4984
5216
  if (typeof rule !== "string") return true;
4985
- return !removedGroupNames.has(getRuleTarget(rule));
5217
+ return !removedTargets.has(getRuleTarget(rule));
4986
5218
  });
4987
5219
  }
4988
5220
  }
@@ -5357,6 +5589,12 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5357
5589
  ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
5358
5590
  { stdio: "inherit" }
5359
5591
  );
5592
+ if (curlResult.error) {
5593
+ if (curlResult.error.code === "ENOENT") {
5594
+ throw new Error("\u672A\u627E\u5230 curl \u547D\u4EE4\uFF0C\u8BF7\u5148\u5B89\u88C5 curl \u540E\u91CD\u8BD5");
5595
+ }
5596
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25: ${curlResult.error.message}`);
5597
+ }
5360
5598
  if (curlResult.status !== 0) {
5361
5599
  try {
5362
5600
  fs7.unlinkSync(tempPath);
@@ -5374,6 +5612,15 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5374
5612
  let extractedBinary = null;
5375
5613
  try {
5376
5614
  if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5615
+ const listResult = spawnSync5("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
5616
+ if (listResult.error) throw listResult.error;
5617
+ if (listResult.status !== 0) throw new Error(`tar \u5217\u8868\u9000\u51FA\u7801 ${listResult.status}`);
5618
+ const entries = (listResult.stdout || "").split("\n").filter(Boolean);
5619
+ for (const entry of entries) {
5620
+ if (entry.startsWith("/") || entry.split("/").includes("..")) {
5621
+ throw new Error(`\u5F52\u6863\u542B\u975E\u6CD5\u8DEF\u5F84\u6761\u76EE: ${entry}`);
5622
+ }
5623
+ }
5377
5624
  const tarResult = spawnSync5("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
5378
5625
  if (tarResult.error) throw tarResult.error;
5379
5626
  if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
@@ -5568,7 +5815,7 @@ function cmdLogs(args) {
5568
5815
  }
5569
5816
 
5570
5817
  // src/commands/overwrite.ts
5571
- import path8 from "path";
5818
+ import path7 from "path";
5572
5819
 
5573
5820
  // src/runtime.ts
5574
5821
  function getRuntimeMode() {
@@ -5596,6 +5843,78 @@ async function launchOrRestart(mode) {
5596
5843
  return result.pid;
5597
5844
  }
5598
5845
 
5846
+ // src/progress.ts
5847
+ var IS_TTY = process.stdout.isTTY === true;
5848
+ var BAR_WIDTH = 20;
5849
+ function createProgressPrinter(totalRounds = 1) {
5850
+ let alive = 0;
5851
+ let dead = 0;
5852
+ const resultMap = /* @__PURE__ */ new Map();
5853
+ function render(done, total) {
5854
+ if (!IS_TTY) return;
5855
+ const pct = Math.round(done / total * 100);
5856
+ const filled = Math.round(done / total * BAR_WIDTH);
5857
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
5858
+ process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
5859
+ }
5860
+ return {
5861
+ onResult(result, index, total, round = 1) {
5862
+ if (resultMap.size === 0 && totalRounds > 1) {
5863
+ console.log(`--- \u7B2C 1 \u8F6E\u6D4B\u8BD5 (${total} \u4E2A\u8282\u70B9) ---`);
5864
+ }
5865
+ const prev = resultMap.get(result.name);
5866
+ if (prev) {
5867
+ if (prev.result.delay !== null) alive--;
5868
+ else dead--;
5869
+ }
5870
+ if (result.delay !== null) alive++;
5871
+ else dead++;
5872
+ resultMap.set(result.name, { result, round });
5873
+ render(index + 1, total);
5874
+ },
5875
+ onRetryRound(round, count) {
5876
+ if (IS_TTY) {
5877
+ process.stdout.write("\n");
5878
+ }
5879
+ console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
5880
+ alive = 0;
5881
+ dead = 0;
5882
+ },
5883
+ finish() {
5884
+ if (IS_TTY) {
5885
+ process.stdout.write("\n");
5886
+ }
5887
+ console.log("");
5888
+ if (!IS_TTY) return;
5889
+ const entries = [...resultMap.values()];
5890
+ entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
5891
+ const total = entries.length;
5892
+ console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
5893
+ for (let i = 0; i < entries.length; i++) {
5894
+ const { result, round } = entries[i];
5895
+ const prefix = `[${i + 1}/${total}]`;
5896
+ if (result.delay !== null) {
5897
+ const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
5898
+ const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
5899
+ console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
5900
+ } else {
5901
+ console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
5902
+ }
5903
+ }
5904
+ console.log("");
5905
+ }
5906
+ };
5907
+ }
5908
+ function formatCleanSummary(result) {
5909
+ const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
5910
+ if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
5911
+ if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
5912
+ return parts.join(", ");
5913
+ }
5914
+ function formatTestSummary(summary) {
5915
+ return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
5916
+ }
5917
+
5599
5918
  // src/commands/status.ts
5600
5919
  function printStatus() {
5601
5920
  const status = getStatus();
@@ -5607,7 +5926,7 @@ function printStatus() {
5607
5926
  const { running, pid, daemon: daemonManaged } = state;
5608
5927
  console.log("");
5609
5928
  let modeLabel = "";
5610
- if (info && running) {
5929
+ if (info) {
5611
5930
  modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
5612
5931
  }
5613
5932
  const statusText = running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
@@ -5620,7 +5939,10 @@ function printStatus() {
5620
5939
  }
5621
5940
  }
5622
5941
  if (info) {
5623
- if (info.mixedPort) {
5942
+ if (info.tun) {
5943
+ const extra = info.mixedPort ? `\uFF0C\u53E6\u76D1\u542C ${info.mixedPort}` : "";
5944
+ console.log(`${colors.gray("\u7AEF\u53E3: ")}TUN \u63A5\u7BA1${extra}`);
5945
+ } else if (info.mixedPort) {
5624
5946
  console.log(`${colors.gray("\u7AEF\u53E3: ")}${info.mixedPort}`);
5625
5947
  } else {
5626
5948
  const ports = [];
@@ -5676,903 +5998,851 @@ async function cmdStop() {
5676
5998
  console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
5677
5999
  }
5678
6000
 
5679
- // src/test-instance.ts
5680
- import { spawn as spawn2 } from "child_process";
5681
- import fs8 from "fs";
5682
- import path7 from "path";
5683
-
5684
- // src/lifecycle.ts
5685
- var cleanupFns = /* @__PURE__ */ new Set();
5686
- function registerCleanup(fn) {
5687
- cleanupFns.add(fn);
5688
- return () => {
5689
- cleanupFns.delete(fn);
5690
- };
5691
- }
5692
- function runCleanup() {
5693
- for (const fn of cleanupFns) {
5694
- try {
5695
- fn();
5696
- } catch {
5697
- }
6001
+ // src/commands/start.ts
6002
+ async function cmdStart(args) {
6003
+ if (!hasKernel()) {
6004
+ console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
6005
+ process.exit(1);
5698
6006
  }
5699
- cleanupFns.clear();
5700
- }
5701
-
5702
- // src/test-instance.ts
5703
- var TEST_DIR = path7.join(USER_DATA_DIR, "test");
5704
- var TEST_DIRS = {
5705
- data: path7.join(TEST_DIR, "data"),
5706
- runtime: path7.join(TEST_DIR, "runtime")
5707
- };
5708
- var TEST_PATHS = {
5709
- configFile: path7.join(TEST_DIRS.runtime, "config.yaml"),
5710
- pidFile: path7.join(TEST_DIRS.runtime, "pid"),
5711
- logFile: path7.join(TEST_DIR, "test.log")
5712
- };
5713
- var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
5714
- function ensureTestDirs() {
5715
- for (const dir of Object.values(TEST_DIRS)) {
5716
- fs8.mkdirSync(dir, { recursive: true, mode: 448 });
6007
+ const targetMode = args[1] === "tun" ? "tun" : "mixed";
6008
+ const daemonEnabled = isDaemonEnabled();
6009
+ if (targetMode === "tun" && daemonEnabled) {
6010
+ console.error(`${colors.red("\u9519\u8BEF:")} \u4FDD\u6D3B\u5DF2\u542F\u7528\uFF08\u4EC5\u652F\u6301 Mixed \u6A21\u5F0F\uFF09\uFF0C\u65E0\u6CD5\u542F\u52A8 TUN`);
6011
+ console.error("\u8BF7\u5148\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
6012
+ process.exit(1);
5717
6013
  }
5718
- }
5719
- function cleanupTestDir() {
5720
- rmrf(TEST_DIR);
5721
- }
5722
- function buildTestConfig(subName) {
5723
- ensureTestDirs();
5724
- const rawContent = readSubscriptionRawConfig(subName);
5725
- if (!rawContent) {
5726
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
6014
+ const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6015
+ const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6016
+ const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
6017
+ const skipUpdate = hasFlag(args, "-s", "--no-update");
6018
+ const skipClean = hasFlag(args, "--no-clean");
6019
+ const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
6020
+ const sub = getActiveSubscription();
6021
+ if (!sub) {
6022
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
6023
+ process.exit(1);
5727
6024
  }
5728
- const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
5729
- const proxies = (parsed.proxies || []).filter(isProxyValid);
5730
- if (proxies.length === 0) {
5731
- throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
6025
+ if (!skipUpdate) {
6026
+ await autoUpdateStaleSubscription({ timeout: updateTimeout });
5732
6027
  }
5733
- const nameCount = /* @__PURE__ */ new Map();
5734
- for (const proxy of proxies) {
5735
- const count = (nameCount.get(proxy.name) || 0) + 1;
5736
- nameCount.set(proxy.name, count);
5737
- if (count > 1) {
5738
- proxy.name = `${proxy.name} #${count}`;
6028
+ if (!daemonEnabled) {
6029
+ if (hasRootResidue()) {
6030
+ console.error(`${colors.red("\u9519\u8BEF:")} \u5B58\u5728\u9700\u8981 root \u6743\u9650\u6E05\u7406\u7684\u6B8B\u7559\u8FDB\u7A0B/\u6587\u4EF6`);
6031
+ console.error(`\u8BF7\u5148\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo && sudo rm -f ${PATHS.pidFile}`);
6032
+ console.error("\u6216\u5207\u6362\u5230 TUN \u6A21\u5F0F\u542F\u52A8\uFF08\u81EA\u52A8\u6E05\u7406\uFF09: mihomo start tun");
6033
+ process.exit(1);
5739
6034
  }
5740
- }
5741
- const config = {
5742
- ...TEST_CONFIG,
5743
- proxies,
5744
- "proxy-groups": [
5745
- {
5746
- name: "PROXY",
5747
- type: "select",
5748
- proxies: proxies.map((p) => p.name)
5749
- }
5750
- ],
5751
- rules: ["MATCH,PROXY"]
5752
- };
5753
- const content = dumpYaml(config);
5754
- fs8.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
5755
- }
5756
- async function startTestInstance() {
5757
- const binary = PATHS.mihomoBinary;
5758
- if (!fs8.existsSync(binary)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
5759
- stopTestInstance();
5760
- const logFd = fs8.openSync(TEST_PATHS.logFile, "a");
5761
- const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
5762
- detached: true,
5763
- stdio: ["ignore", logFd, logFd]
5764
- });
5765
- fs8.closeSync(logFd);
5766
- child.unref();
5767
- const pid = child.pid;
5768
- fs8.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
5769
- const client = createHttpClient({ timeout: 2e3 });
5770
- let ready = false;
5771
- for (let i = 0; i < 60; i++) {
5772
- if (!isProcessRunning(pid)) break;
5773
- try {
5774
- await client.get(`${TEST_API}/version`);
5775
- ready = true;
5776
- break;
5777
- } catch {
5778
- await sleep(500);
6035
+ const status = getStatus();
6036
+ const hasProcess = status.running || status.allProcesses.length > 0;
6037
+ if (hasProcess) {
6038
+ const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
6039
+ console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
5779
6040
  }
5780
- }
5781
- if (!isProcessRunning(pid)) {
5782
- let errorDetail = "";
5783
- try {
5784
- errorDetail = fs8.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
5785
- } catch {
6041
+ handleStopResult(stop());
6042
+ if (hasProcess) {
6043
+ console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
6044
+ `);
5786
6045
  }
5787
- throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
5788
- ${errorDetail}` : ""}`);
5789
6046
  }
5790
- if (!ready) {
5791
- throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
5792
- }
5793
- }
5794
- function stopTestInstance() {
5795
- let pid;
6047
+ let configInfo;
5796
6048
  try {
5797
- pid = parseInt(fs8.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
5798
- } catch {
5799
- return;
5800
- }
5801
- if (pid > 0 && isProcessRunning(pid)) {
5802
- process.kill(pid, "SIGKILL");
5803
- for (let i = 0; i < 20; i++) {
5804
- if (!isProcessRunning(pid)) break;
5805
- sleepSync(100);
5806
- }
6049
+ configInfo = prepareConfigForStart(targetMode, sub.name);
6050
+ } catch (e) {
6051
+ console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
6052
+ process.exit(1);
5807
6053
  }
6054
+ const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
6055
+ console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
5808
6056
  try {
5809
- fs8.unlinkSync(TEST_PATHS.pidFile);
5810
- } catch {
6057
+ const pid = await launchOrRestart(targetMode);
6058
+ const label = daemonEnabled ? "\u5DF2\u542F\u52A8 (\u4FDD\u6D3B)" : "\u5DF2\u542F\u52A8";
6059
+ console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""}`);
6060
+ } catch (e) {
6061
+ const msg = e.message;
6062
+ const lines = msg.split("\n");
6063
+ console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${lines[0]}`);
6064
+ if (lines.length > 1) {
6065
+ for (const line of lines.slice(1)) console.error(line);
6066
+ }
6067
+ process.exit(1);
5811
6068
  }
5812
- }
5813
- async function withTestInstance(subName, fn) {
5814
- cleanupTestDir();
5815
- buildTestConfig(subName);
5816
- const unregister = registerCleanup(() => {
5817
- stopTestInstance();
5818
- cleanupTestDir();
5819
- });
5820
- try {
5821
- await startTestInstance();
5822
- return await fn(TEST_API);
5823
- } finally {
5824
- unregister();
5825
- stopTestInstance();
5826
- cleanupTestDir();
6069
+ const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
6070
+ if (!skipClean && configInfo.proxies > cleanThreshold) {
6071
+ const cache = readSubscriptionCache();
6072
+ const lastCleanAt = cache[sub.name]?.last_auto_clean_at;
6073
+ const withinCooldown = !!lastCleanAt && Date.now() - new Date(lastCleanAt).getTime() < AUTO_CLEAN_COOLDOWN_HOURS * 60 * 60 * 1e3;
6074
+ if (!withinCooldown) {
6075
+ console.log("");
6076
+ console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${cleanThreshold}\uFF0C\u81EA\u52A8\u6E05\u7406\uFF08${AUTO_CLEAN_COOLDOWN_HOURS}h \u5185\u4EC5\u4E00\u6B21\uFF0C--no-clean \u8DF3\u8FC7\uFF09...`);
6077
+ console.log("");
6078
+ await sleep(1e3);
6079
+ const progress = createProgressPrinter(rounds);
6080
+ const cleanResult = await autoCleanSubscription(sub.name, {
6081
+ timeout,
6082
+ concurrency,
6083
+ rounds,
6084
+ onResult: progress.onResult,
6085
+ onRetryRound: progress.onRetryRound
6086
+ });
6087
+ progress.finish();
6088
+ console.log(formatTestSummary(cleanResult.summary));
6089
+ if (cleanResult.skipped) {
6090
+ 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"));
6091
+ } else if (cleanResult.removedProxies > 0) {
6092
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
6093
+ console.log("");
6094
+ console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
6095
+ if (!daemonEnabled) handleStopResult(stop());
6096
+ try {
6097
+ configInfo = prepareConfigForStart(targetMode, sub.name);
6098
+ const pid = await launchOrRestart(targetMode);
6099
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
6100
+ } catch (e) {
6101
+ console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6102
+ process.exit(1);
6103
+ }
6104
+ }
6105
+ saveSubscriptionCache(sub.name, { last_auto_clean_at: (/* @__PURE__ */ new Date()).toISOString() });
6106
+ }
5827
6107
  }
6108
+ printStatus();
5828
6109
  }
5829
6110
 
5830
- // src/commands/subscription.ts
5831
- var IS_TTY = process.stdout.isTTY === true;
5832
- var BAR_WIDTH = 20;
5833
- function createProgressPrinter(totalRounds = 1) {
5834
- let alive = 0;
5835
- let dead = 0;
5836
- const resultMap = /* @__PURE__ */ new Map();
5837
- function render(done, total) {
5838
- if (!IS_TTY) return;
5839
- const pct = Math.round(done / total * 100);
5840
- const filled = Math.round(done / total * BAR_WIDTH);
5841
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
5842
- process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
5843
- }
5844
- return {
5845
- onResult(result, index, total, round = 1) {
5846
- if (resultMap.size === 0 && totalRounds > 1) {
5847
- console.log(`--- \u7B2C 1 \u8F6E\u6D4B\u8BD5 (${total} \u4E2A\u8282\u70B9) ---`);
5848
- }
5849
- if (result.delay !== null) alive++;
5850
- else dead++;
5851
- resultMap.set(result.name, { result, round });
5852
- render(index + 1, total);
5853
- },
5854
- onRetryRound(round, count) {
5855
- if (IS_TTY) {
5856
- process.stdout.write("\n");
6111
+ // src/commands/overwrite.ts
6112
+ function printOverwriteList() {
6113
+ const info = listOverwriteFile();
6114
+ const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
6115
+ console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}`);
6116
+ console.log(`${colors.gray("\u4F4D\u7F6E: ")}${info.dir}`);
6117
+ console.log("");
6118
+ if (info.files.length === 0) {
6119
+ console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
6120
+ console.log("");
6121
+ console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path7.join(info.dir, "overwrite.yaml")}`);
6122
+ console.log(` \u6216 ${path7.join(info.dir, "overwrite.dns.yaml")}`);
6123
+ console.log("");
6124
+ } else {
6125
+ console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
6126
+ console.log("");
6127
+ info.files.forEach((f, i) => {
6128
+ const num = i < 10 ? ` ${i}` : `${i}`;
6129
+ console.log(` ${num}. ${f.name}`);
6130
+ if (f.scope) {
6131
+ console.log(` ${colors.gray("\u4F5C\u7528\u57DF: ")}${f.scope}`);
5857
6132
  }
5858
- console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
5859
- alive = 0;
5860
- dead = 0;
5861
- },
5862
- finish() {
5863
- if (IS_TTY) {
5864
- process.stdout.write("\n");
6133
+ if (f.keys.length > 0) {
6134
+ console.log(` ${colors.gray("\u5B57\u6BB5: ")}${f.keys.join(", ")}`);
5865
6135
  }
6136
+ });
6137
+ console.log("");
6138
+ }
6139
+ console.log("\u542F\u7528\u8986\u5199: mihomo ow on");
6140
+ console.log("\u7981\u7528\u8986\u5199: mihomo ow off");
6141
+ console.log("");
6142
+ }
6143
+ async function cmdOverwrite(args) {
6144
+ const action = args?.[1];
6145
+ const currentMode = getRuntimeMode();
6146
+ const restartNeeded = isRestartNeededOnChange();
6147
+ if (action === "on" || action === "enable") {
6148
+ if (isOverwriteEnabled()) {
6149
+ console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u542F\u7528\u72B6\u6001");
5866
6150
  console.log("");
5867
- const entries = [...resultMap.values()];
5868
- entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
5869
- const total = entries.length;
5870
- console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
5871
- for (let i = 0; i < entries.length; i++) {
5872
- const { result, round } = entries[i];
5873
- const prefix = `[${i + 1}/${total}]`;
5874
- if (result.delay !== null) {
5875
- const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
5876
- const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
5877
- console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
5878
- } else {
5879
- console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
5880
- }
5881
- }
6151
+ printOverwriteList();
6152
+ return;
6153
+ }
6154
+ setOverwriteEnabled(true);
6155
+ console.log("\u5DF2\u542F\u7528\u8986\u5199\u914D\u7F6E");
6156
+ if (restartNeeded) {
5882
6157
  console.log("");
6158
+ await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
6159
+ return;
5883
6160
  }
5884
- };
5885
- }
5886
- function formatCleanSummary(result) {
5887
- const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
5888
- if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
5889
- if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
5890
- return parts.join(", ");
5891
- }
5892
- function formatTestSummary(summary) {
5893
- return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
5894
- }
5895
- function githubRepoUrl(rawUrl) {
5896
- const match = rawUrl.match(/raw\.githubusercontent\.com\/([^/]+\/[^/]+)/);
5897
- if (match) return `https://github.com/${match[1]}`;
5898
- return null;
5899
- }
5900
- function resolveTestTarget(args) {
5901
- const subs = getSubscriptions();
5902
- if (subs.length === 0) {
5903
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5904
- process.exit(1);
6161
+ console.log("");
6162
+ printOverwriteList();
6163
+ return;
5905
6164
  }
5906
- const nameArg = getNonFlagArg(args, 2);
5907
- const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
5908
- const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
5909
- let target;
5910
- if (nameArg) {
5911
- const matches = findSubscriptionFuzzy(subs, nameArg);
5912
- target = pickSingleSubscription(matches, nameArg);
5913
- } else {
5914
- const activeSub = getActiveSubscription();
5915
- if (!activeSub) {
5916
- console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
5917
- process.exit(1);
6165
+ if (action === "off" || action === "disable") {
6166
+ if (!isOverwriteEnabled()) {
6167
+ console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u7981\u7528\u72B6\u6001");
6168
+ console.log("");
6169
+ printOverwriteList();
6170
+ return;
6171
+ }
6172
+ setOverwriteEnabled(false);
6173
+ console.log("\u5DF2\u7981\u7528\u8986\u5199\u914D\u7F6E");
6174
+ if (restartNeeded) {
6175
+ console.log("");
6176
+ await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
6177
+ return;
5918
6178
  }
5919
- target = activeSub;
5920
- }
5921
- return { target, timeout, concurrency };
5922
- }
5923
- function printRestartHintIfRunning() {
5924
- if (getRunningState().running) {
5925
- console.log(colors.yellow("\u63D0\u793A: \u8FD0\u884C\u4E2D\u7684\u5B9E\u4F8B\u4ECD\u4F7F\u7528\u65E7\u914D\u7F6E\uFF0C\u6267\u884C mihomo start \u4F7F\u66F4\u65B0\u751F\u6548"));
5926
6179
  console.log("");
6180
+ printOverwriteList();
6181
+ return;
5927
6182
  }
6183
+ console.log("");
6184
+ printOverwriteList();
5928
6185
  }
5929
- async function printSubscriptionList(options) {
5930
- if (options?.autoUpdate !== false) {
5931
- const updateResult = await autoUpdateStaleSubscription();
5932
- if (updateResult.total > 0) console.log("");
5933
- }
5934
- const subs = getSubscriptionsWithCache();
5935
- if (subs.length === 0) {
5936
- console.log("\u6CA1\u6709\u8BA2\u9605");
5937
- console.log("");
5938
- console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
5939
- console.log("");
5940
- return;
6186
+
6187
+ // src/commands/reset.ts
6188
+ import fs8 from "fs";
6189
+ import readline from "readline";
6190
+ var RESET_TARGETS = [
6191
+ {
6192
+ id: "subs",
6193
+ aliases: ["sub", "subs", "subscription", "subscriptions"],
6194
+ label: "\u8BA2\u9605",
6195
+ paths: () => [DIRS.subscriptions],
6196
+ needsStop: true,
6197
+ // 同步清空 settings 里的订阅列表:只删缓存文件会留下"列表存在但无配置"的半重置状态
6198
+ // (start 会报"未找到订阅配置")。active_subscription 一并清除
6199
+ onAfter: () => writeSettings({ subscriptions: void 0, active_subscription: void 0 })
6200
+ },
6201
+ {
6202
+ id: "logs",
6203
+ aliases: ["log", "logs"],
6204
+ label: "\u65E5\u5FD7",
6205
+ paths: () => [DIRS.logs],
6206
+ needsStop: false
6207
+ },
6208
+ {
6209
+ id: "data",
6210
+ aliases: ["data"],
6211
+ label: "\u8FD0\u884C\u6570\u636E",
6212
+ paths: () => [DIRS.data],
6213
+ needsStop: true
6214
+ },
6215
+ {
6216
+ id: "runtime",
6217
+ aliases: ["runtime"],
6218
+ label: "\u8FD0\u884C\u65F6",
6219
+ paths: () => [DIRS.runtime],
6220
+ needsStop: true
6221
+ },
6222
+ {
6223
+ id: "settings",
6224
+ aliases: ["setting", "settings", "config"],
6225
+ label: "\u8BBE\u7F6E",
6226
+ paths: () => [PATHS.settingsFile],
6227
+ needsStop: false
6228
+ },
6229
+ {
6230
+ id: "kernel",
6231
+ aliases: ["kernel", "core"],
6232
+ label: "\u5185\u6838",
6233
+ paths: () => [DIRS.kernel],
6234
+ needsStop: false,
6235
+ onAfter: () => clearKernelVersionCache(),
6236
+ checkEmpty: () => !hasKernel(),
6237
+ emptyMsg: "\u5185\u6838\u672A\u5B89\u88C5\uFF0C\u65E0\u9700\u5220\u9664",
6238
+ warnIfRunning: true
6239
+ },
6240
+ {
6241
+ id: "overwrites",
6242
+ aliases: ["overwrite", "overwrites", "ow"],
6243
+ label: "\u8986\u5199",
6244
+ paths: () => {
6245
+ const dir = USER_DATA_DIR;
6246
+ if (!fs8.existsSync(dir)) return [];
6247
+ return fs8.readdirSync(dir).filter(isOverwriteFilename).map((f) => `${dir}/${f}`);
6248
+ },
6249
+ needsStop: false
6250
+ },
6251
+ {
6252
+ id: "daemon",
6253
+ aliases: ["daemon"],
6254
+ label: "\u4FDD\u6D3B",
6255
+ // 卸载由确认后的 disablesDaemon 段统一处理(需 sudo,受取消保护);
6256
+ // 此处 paths 返回空(plist 在系统目录,用户态删不掉,且不应提前删破坏卸载),
6257
+ // onAfter 因幂等守卫(plist 已删)成为 no-op,仅作单独 reset 未走前段时的兜底。
6258
+ paths: () => [],
6259
+ needsStop: false,
6260
+ onAfter: () => disableDaemon(),
6261
+ checkEmpty: () => !isDaemonEnabled(),
6262
+ emptyMsg: "\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u9700\u5220\u9664"
5941
6263
  }
5942
- const activeSub = getActiveSubscription();
5943
- console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
5944
- subs.forEach((s, i) => {
5945
- const time = formatDate(s.updated_at);
5946
- const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
5947
- const mergeBadge = isMultiUrl(s.url) ? colors.cyan(` [\u5408\u5E76 ${splitUrls(s.url).length} \u6E90]`) : "";
5948
- const interval = resolveUpdateInterval(s.url, s.update_interval);
5949
- console.log(` ${i + 1}. ${s.name}${defaultMark}${mergeBadge}`);
5950
- console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
5951
- if (s.username) {
5952
- console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
5953
- }
5954
- if (s.download !== void 0 || s.total !== void 0) {
5955
- const used = (s.upload || 0) + (s.download || 0);
5956
- const usedStr = formatBytes(used);
5957
- const totalStr = formatBytes(s.total);
5958
- let percentStr = "";
5959
- if (s.total && s.total > 0) {
5960
- const percent = Math.min(used / s.total * 100, 100);
5961
- percentStr = ` (${percent.toFixed(1)}%)`;
5962
- }
5963
- console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
5964
- }
5965
- if (s.expire !== void 0) {
5966
- console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
5967
- }
5968
- if (s.web_page_url) {
5969
- console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
6264
+ ];
6265
+ function resolveResetTargets(names) {
6266
+ const matched = [];
6267
+ const unmatched = [];
6268
+ for (const name of names) {
6269
+ const t = RESET_TARGETS.find((t2) => t2.aliases.includes(name.toLowerCase()));
6270
+ if (t) {
6271
+ if (!matched.find((m) => m.id === t.id)) matched.push(t);
6272
+ } else {
6273
+ unmatched.push(name);
5970
6274
  }
6275
+ }
6276
+ return { matched, unmatched };
6277
+ }
6278
+ async function confirmPrompt(question) {
6279
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6280
+ const answer = await new Promise((resolve) => {
6281
+ rl.question(`${question} (y/N) `, (a) => {
6282
+ rl.close();
6283
+ resolve(a);
6284
+ });
5971
6285
  });
5972
- console.log("");
5973
- console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
5974
- console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
5975
- console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
5976
- console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
5977
- console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
5978
- console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
5979
- console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
5980
- console.log("");
6286
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
5981
6287
  }
5982
- async function cmdSubscription(args) {
5983
- const action = args[1];
5984
- if (!action || action === "list") {
5985
- await printSubscriptionList();
5986
- return;
5987
- }
5988
- if (action === "add") {
5989
- const url = args[2];
5990
- const name = args[3] || "default";
5991
- if (!url) {
5992
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
5993
- process.exit(1);
5994
- }
5995
- if (isMultiUrl(url)) {
5996
- const urls = splitUrls(url);
5997
- for (const u of urls) {
5998
- if (!u.startsWith("http")) {
5999
- console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
6000
- process.exit(1);
6001
- }
6002
- }
6003
- console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
6004
- try {
6005
- addSubscription(url, name);
6006
- setDefaultSubscription(name);
6007
- const info = await downloadMergedSubscription(urls, name);
6008
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
6009
- } catch (e) {
6010
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
6011
- process.exit(1);
6012
- }
6013
- } else {
6014
- if (!url.startsWith("http")) {
6015
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6016
- process.exit(1);
6017
- }
6018
- console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
6019
- try {
6020
- addSubscription(url, name);
6021
- setDefaultSubscription(name);
6022
- const info = await downloadSubscription(url, name);
6023
- const repoUrl = githubRepoUrl(url);
6024
- if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
6025
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
6026
- } catch (e) {
6027
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
6028
- process.exit(1);
6029
- }
6030
- }
6288
+ async function cmdReset(args) {
6289
+ const flags = (args || []).filter((a) => a.startsWith("-"));
6290
+ const names = (args || []).slice(1).filter((a) => !a.startsWith("-"));
6291
+ const KNOWN_FLAGS = /* @__PURE__ */ new Set(["--full", "--yes", "-y"]);
6292
+ const unknownFlags = flags.filter((f) => !KNOWN_FLAGS.has(f));
6293
+ if (unknownFlags.length > 0) {
6294
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u9009\u9879: ${unknownFlags.join(", ")}`);
6031
6295
  console.log("");
6032
- await printSubscriptionList();
6033
- return;
6296
+ console.log("\u53EF\u7528\u9009\u9879: --full\uFF08\u5220\u5168\u90E8\uFF09, -y/--yes\uFF08\u8DF3\u8FC7\u786E\u8BA4\uFF09");
6297
+ process.exit(1);
6034
6298
  }
6035
- if (action === "update") {
6036
- const name = args[2];
6037
- const subs = getSubscriptions();
6038
- if (subs.length === 0) {
6039
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6040
- process.exit(1);
6041
- }
6042
- if (!name) {
6043
- console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
6044
- const results = await Promise.all(subs.map((sub) => tryUpdateOne(sub)));
6045
- let ok = 0;
6046
- for (const r of results) {
6047
- if (r.success) ok++;
6048
- printUpdateResult(r);
6049
- }
6050
- if (ok === 0) process.exit(1);
6299
+ const fullReset = flags.includes("--full");
6300
+ const skipConfirm = flags.includes("--yes") || flags.includes("-y");
6301
+ let targets;
6302
+ if (fullReset) {
6303
+ targets = RESET_TARGETS;
6304
+ } else if (names.length > 0) {
6305
+ const { matched, unmatched } = resolveResetTargets(names);
6306
+ if (unmatched.length > 0) {
6307
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u91CD\u7F6E\u76EE\u6807: ${unmatched.join(", ")}`);
6051
6308
  console.log("");
6052
- printRestartHintIfRunning();
6053
- await printSubscriptionList();
6054
- return;
6055
- }
6056
- const matches = findSubscriptionFuzzy(subs, name);
6057
- const target = pickSingleSubscription(matches, name);
6058
- console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
6059
- const result = await tryUpdateOne(target);
6060
- if (!result.success) {
6061
- console.error(`\u66F4\u65B0\u5931\u8D25: ${(result.error || "").split("\n")[0]}`);
6062
- process.exit(1);
6063
- }
6064
- console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(result)})`);
6065
- console.log("");
6066
- printRestartHintIfRunning();
6067
- await printSubscriptionList();
6068
- return;
6069
- }
6070
- if (action === "use") {
6071
- const name = args[2];
6072
- const subs = getSubscriptions();
6073
- if (!name) {
6074
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
6075
- if (subs.length > 0) {
6076
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
6077
- for (const s of subs) console.log(` ${s.name}`);
6078
- }
6079
- process.exit(1);
6080
- }
6081
- const matches = findSubscriptionFuzzy(subs, name);
6082
- const target = pickSingleSubscription(matches, name);
6083
- const currentDefault = getActiveSubscription();
6084
- const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
6085
- if (isAlreadyDefault) {
6086
- console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
6309
+ console.log(`\u53EF\u7528\u76EE\u6807: ${RESET_TARGETS.map((t) => t.aliases[0]).join(", ")}`);
6087
6310
  console.log("");
6088
- await printSubscriptionList();
6089
- return;
6090
- }
6091
- const currentMode = getRuntimeMode();
6092
- const restartNeeded = isRestartNeededOnChange();
6093
- const success = setDefaultSubscription(target.name);
6094
- if (success) {
6095
- console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
6096
- } else {
6097
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
6311
+ console.log("\u793A\u4F8B:");
6312
+ console.log(" mihomo reset sub log # \u5220\u9664\u8BA2\u9605\u548C\u65E5\u5FD7");
6313
+ console.log(" mihomo reset kernel # \u53EA\u5220\u5185\u6838");
6314
+ console.log(" mihomo reset --full # \u5220\u9664\u5168\u90E8");
6315
+ console.log(" mihomo reset # \u5220\u9664\u5168\u90E8\uFF08\u4FDD\u7559\u8BBE\u7F6E\u3001\u5185\u6838\u3001\u8986\u5199\uFF09");
6098
6316
  process.exit(1);
6099
6317
  }
6100
- if (restartNeeded) {
6101
- console.log("");
6102
- await cmdStart(["start", currentMode]);
6103
- return;
6104
- }
6105
- console.log("");
6106
- await printSubscriptionList();
6107
- return;
6318
+ targets = matched;
6319
+ } else {
6320
+ targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
6108
6321
  }
6109
- if (action === "web" || action === "open") {
6110
- const name = args[2];
6111
- const subs = getSubscriptionsWithCache();
6112
- if (subs.length === 0) {
6113
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6114
- process.exit(1);
6115
- }
6116
- let target;
6117
- if (name) {
6118
- const matches = findSubscriptionFuzzy(subs, name);
6119
- target = pickSingleSubscription(matches, name);
6120
- } else {
6121
- target = getActiveSubscription() || subs[0];
6122
- }
6123
- const cached = subs.find((s) => s.name === target.name);
6124
- let webPageUrl = cached?.web_page_url;
6125
- if (!webPageUrl) {
6126
- console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u66F4\u65B0\u8BA2\u9605...");
6127
- try {
6128
- await downloadSubscription(target.url, target.name);
6129
- const cache = readSubscriptionCache();
6130
- if (cache[target.name]?.web_page_url) {
6131
- webPageUrl = cache[target.name].web_page_url;
6132
- } else {
6133
- console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
6134
- process.exit(1);
6135
- }
6136
- } catch (e) {
6137
- console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
6138
- process.exit(1);
6322
+ for (const t of targets) {
6323
+ if (t.checkEmpty?.()) {
6324
+ if (targets.length === 1) {
6325
+ console.log(t.emptyMsg);
6326
+ return;
6139
6327
  }
6140
6328
  }
6141
- console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
6142
- const opened = openUrl(webPageUrl);
6143
- if (!opened) {
6144
- console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
6145
- }
6329
+ }
6330
+ const needsStop = targets.some((t) => t.needsStop);
6331
+ const warnRunning = targets.some((t) => t.warnIfRunning);
6332
+ const kernelTargeted = targets.some((t) => t.id === "kernel");
6333
+ const daemonTargeted = targets.some((t) => t.id === "daemon");
6334
+ const disablesDaemon = needsStop || kernelTargeted || daemonTargeted;
6335
+ const pids = needsStop || warnRunning ? getMihomoPids() : [];
6336
+ if (warnRunning && pids.length > 0) {
6337
+ console.log(colors.yellow(`\u8B66\u544A: mihomo \u6B63\u5728\u8FD0\u884C (PID ${pids.join(", ")})\uFF0C\u5220\u9664\u5185\u6838\u540E\u5C06\u65E0\u6CD5\u91CD\u65B0\u542F\u52A8`));
6338
+ }
6339
+ if (disablesDaemon && isDaemonEnabled()) {
6340
+ console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u91CD\u7F6E\u5C06\u4E00\u5E76\u5173\u95ED\u4FDD\u6D3B\uFF08\u79FB\u9664\u5F00\u673A\u81EA\u542F\uFF09"));
6341
+ }
6342
+ console.log(`\u5C06\u5220\u9664: ${targets.map((t) => t.label).join("\u3001")}`);
6343
+ if (!skipConfirm && !await confirmPrompt("\u786E\u8BA4?")) {
6344
+ console.log("\u5DF2\u53D6\u6D88");
6146
6345
  return;
6147
6346
  }
6148
- if (action === "remove" || action === "rm" || action === "delete") {
6149
- const name = args[2];
6150
- const subs = getSubscriptions();
6151
- if (!name) {
6152
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
6153
- if (subs.length > 0) {
6154
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
6155
- for (const s of subs) console.log(` ${s.name}`);
6156
- }
6157
- process.exit(1);
6347
+ if (disablesDaemon && isDaemonEnabled()) {
6348
+ try {
6349
+ disableDaemon();
6350
+ } catch (e) {
6351
+ console.error(`${colors.red("\u4FDD\u6D3B\u5173\u95ED\u5DF2\u53D6\u6D88\uFF0C\u91CD\u7F6E\u4E2D\u6B62:")} ${e.message.split("\n")[0]}`);
6352
+ return;
6158
6353
  }
6159
- const matches = findSubscriptionFuzzy(subs, name);
6160
- const target = pickSingleSubscription(matches, name);
6161
- const switchedTo = removeSubscription(target.name);
6162
- console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
6163
- if (switchedTo) {
6164
- console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
6354
+ }
6355
+ if (needsStop && getMihomoPids().length > 0) {
6356
+ console.log("\u505C\u6B62\u8FDB\u7A0B...");
6357
+ cleanupAll();
6358
+ for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6359
+ if (getMihomoPids().length === 0) break;
6360
+ await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6165
6361
  }
6166
- console.log("");
6167
- await printSubscriptionList({ autoUpdate: false });
6168
- return;
6169
6362
  }
6170
- if (action === "clean") {
6171
- const { target, timeout, concurrency } = resolveTestTarget(args);
6172
- const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6173
- console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
6174
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6175
- console.log("");
6176
- const progress = createProgressPrinter(rounds);
6177
- const result = await withTestInstance(target.name, async (apiBase) => {
6178
- return autoCleanSubscription(target.name, {
6179
- timeout,
6180
- concurrency,
6181
- rounds,
6182
- apiBase,
6183
- onResult: progress.onResult,
6184
- onRetryRound: progress.onRetryRound
6185
- });
6186
- });
6187
- progress.finish();
6188
- console.log(formatTestSummary(result.summary));
6189
- if (result.skipped) {
6190
- console.log("");
6191
- 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"));
6192
- } else if (result.removedProxies > 0) {
6193
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6194
- if (getRunningState().running) {
6195
- console.log("");
6196
- console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
6363
+ for (const t of targets) {
6364
+ for (const p of t.paths()) {
6365
+ if (fs8.existsSync(p)) {
6366
+ try {
6367
+ rmrf(p);
6368
+ } catch (e) {
6369
+ console.warn(` \u8B66\u544A: \u65E0\u6CD5\u5220\u9664 ${p}: ${e.message}`);
6370
+ }
6197
6371
  }
6198
6372
  }
6199
- return;
6373
+ t.onAfter?.();
6200
6374
  }
6201
- if (action === "test") {
6202
- const { target, timeout, concurrency } = resolveTestTarget(args);
6203
- console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
6204
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6205
- console.log("");
6206
- const progress = createProgressPrinter();
6207
- const summary = await withTestInstance(target.name, async (apiBase) => {
6208
- return testSubscriptionProxies(target.name, {
6209
- timeout,
6210
- concurrency,
6211
- apiBase,
6212
- onResult: progress.onResult
6213
- });
6214
- });
6215
- progress.finish();
6216
- console.log(formatTestSummary(summary));
6217
- return;
6375
+ ensureDirs();
6376
+ if (targets.some((t) => t.id === "settings")) {
6377
+ invalidateSettingsCache();
6218
6378
  }
6219
- console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
6220
- console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
6221
- process.exit(1);
6379
+ console.log(colors.green(`\u5DF2\u91CD\u7F6E: ${targets.map((t) => t.label).join("\u3001")}`));
6222
6380
  }
6223
6381
 
6224
- // src/commands/start.ts
6225
- async function cmdStart(args) {
6226
- if (!hasKernel()) {
6227
- console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
6228
- process.exit(1);
6382
+ // src/test-instance.ts
6383
+ import { spawn as spawn2 } from "child_process";
6384
+ import fs9 from "fs";
6385
+ import path8 from "path";
6386
+ var TEST_DIR = path8.join(USER_DATA_DIR, "test");
6387
+ var TEST_DIRS = {
6388
+ data: path8.join(TEST_DIR, "data"),
6389
+ runtime: path8.join(TEST_DIR, "runtime")
6390
+ };
6391
+ var TEST_PATHS = {
6392
+ configFile: path8.join(TEST_DIRS.runtime, "config.yaml"),
6393
+ pidFile: path8.join(TEST_DIRS.runtime, "pid"),
6394
+ logFile: path8.join(TEST_DIR, "test.log")
6395
+ };
6396
+ var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
6397
+ function ensureTestDirs() {
6398
+ for (const dir of Object.values(TEST_DIRS)) {
6399
+ fs9.mkdirSync(dir, { recursive: true, mode: 448 });
6229
6400
  }
6230
- const targetMode = args[1] === "tun" ? "tun" : "mixed";
6231
- const daemonEnabled = isDaemonEnabled();
6232
- if (targetMode === "tun" && daemonEnabled) {
6233
- console.error(`${colors.red("\u9519\u8BEF:")} \u4FDD\u6D3B\u5DF2\u542F\u7528\uFF08\u4EC5\u652F\u6301 Mixed \u6A21\u5F0F\uFF09\uFF0C\u65E0\u6CD5\u542F\u52A8 TUN`);
6234
- console.error("\u8BF7\u5148\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
6235
- process.exit(1);
6401
+ }
6402
+ function cleanupTestDir() {
6403
+ rmrf(TEST_DIR);
6404
+ }
6405
+ function buildTestConfig(subName) {
6406
+ ensureTestDirs();
6407
+ const rawContent = readSubscriptionRawConfig(subName);
6408
+ if (!rawContent) {
6409
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
6236
6410
  }
6237
- const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6238
- const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6239
- const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
6240
- const skipUpdate = hasFlag(args, "-s", "--no-update");
6241
- const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
6242
- const sub = getActiveSubscription();
6243
- if (!sub) {
6244
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
6245
- process.exit(1);
6411
+ const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
6412
+ const proxies = (parsed.proxies || []).filter(isProxyValid);
6413
+ if (proxies.length === 0) {
6414
+ throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
6246
6415
  }
6247
- if (!skipUpdate) {
6248
- await autoUpdateStaleSubscription({ timeout: updateTimeout });
6416
+ const nameCount = /* @__PURE__ */ new Map();
6417
+ for (const proxy of proxies) {
6418
+ const count = (nameCount.get(proxy.name) || 0) + 1;
6419
+ nameCount.set(proxy.name, count);
6420
+ if (count > 1) {
6421
+ proxy.name = `${proxy.name} #${count}`;
6422
+ }
6249
6423
  }
6250
- if (!daemonEnabled) {
6251
- const status = getStatus();
6252
- const hasProcess = status.running || status.allProcesses.length > 0;
6253
- if (hasProcess) {
6254
- const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
6255
- console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
6424
+ const config = {
6425
+ ...TEST_CONFIG,
6426
+ proxies,
6427
+ "proxy-groups": [
6428
+ {
6429
+ name: "PROXY",
6430
+ type: "select",
6431
+ proxies: proxies.map((p) => p.name)
6432
+ }
6433
+ ],
6434
+ rules: ["MATCH,PROXY"]
6435
+ };
6436
+ const content = dumpYaml(config);
6437
+ fs9.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
6438
+ }
6439
+ async function startTestInstance() {
6440
+ const binary = PATHS.mihomoBinary;
6441
+ if (!fs9.existsSync(binary)) throw new Error('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
6442
+ stopTestInstance();
6443
+ const logFd = fs9.openSync(TEST_PATHS.logFile, "a");
6444
+ const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
6445
+ detached: true,
6446
+ stdio: ["ignore", logFd, logFd]
6447
+ });
6448
+ child.on("error", () => {
6449
+ });
6450
+ fs9.closeSync(logFd);
6451
+ child.unref();
6452
+ const pid = child.pid;
6453
+ if (!pid) throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u521B\u5EFA\u8FDB\u7A0B\uFF08\u5185\u6838\u4E8C\u8FDB\u5236\u53EF\u80FD\u4E0D\u53EF\u6267\u884C\uFF09");
6454
+ fs9.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
6455
+ const client = createHttpClient({ timeout: 2e3 });
6456
+ let ready = false;
6457
+ for (let i = 0; i < 60; i++) {
6458
+ if (!isProcessRunning(pid)) break;
6459
+ try {
6460
+ await client.get(`${TEST_API}/version`);
6461
+ ready = true;
6462
+ break;
6463
+ } catch {
6464
+ await sleep(500);
6256
6465
  }
6257
- handleStopResult(stop());
6258
- if (hasProcess) {
6259
- console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
6260
- `);
6466
+ }
6467
+ if (!isProcessRunning(pid)) {
6468
+ let errorDetail = "";
6469
+ try {
6470
+ errorDetail = fs9.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
6471
+ } catch {
6261
6472
  }
6473
+ throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
6474
+ ${errorDetail}` : ""}`);
6262
6475
  }
6263
- let configInfo;
6264
- try {
6265
- configInfo = prepareConfigForStart(targetMode, sub.name);
6266
- } catch (e) {
6267
- console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
6268
- process.exit(1);
6476
+ if (!ready) {
6477
+ throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
6269
6478
  }
6270
- const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
6271
- console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
6479
+ }
6480
+ function stopTestInstance() {
6481
+ let pid;
6272
6482
  try {
6273
- const pid = await launchOrRestart(targetMode);
6274
- const label = daemonEnabled ? "\u5DF2\u542F\u52A8 (\u4FDD\u6D3B)" : "\u5DF2\u542F\u52A8";
6275
- console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""}`);
6276
- } catch (e) {
6277
- const msg = e.message;
6278
- const lines = msg.split("\n");
6279
- console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${lines[0]}`);
6280
- if (lines.length > 1) {
6281
- for (const line of lines.slice(1)) console.error(line);
6483
+ pid = parseInt(fs9.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
6484
+ } catch {
6485
+ return;
6486
+ }
6487
+ if (pid > 0 && isProcessRunning(pid)) {
6488
+ process.kill(pid, "SIGKILL");
6489
+ for (let i = 0; i < 20; i++) {
6490
+ if (!isProcessRunning(pid)) break;
6491
+ sleepSync(100);
6282
6492
  }
6493
+ }
6494
+ try {
6495
+ fs9.unlinkSync(TEST_PATHS.pidFile);
6496
+ } catch {
6497
+ }
6498
+ }
6499
+ async function withTestInstance(subName, fn) {
6500
+ cleanupTestDir();
6501
+ buildTestConfig(subName);
6502
+ const unregister = registerCleanup(() => {
6503
+ stopTestInstance();
6504
+ cleanupTestDir();
6505
+ });
6506
+ try {
6507
+ await startTestInstance();
6508
+ return await fn(TEST_API);
6509
+ } finally {
6510
+ unregister();
6511
+ stopTestInstance();
6512
+ cleanupTestDir();
6513
+ }
6514
+ }
6515
+
6516
+ // src/commands/subscription.ts
6517
+ function githubRepoUrl(rawUrl) {
6518
+ const match = rawUrl.match(/raw\.githubusercontent\.com\/([^/]+\/[^/]+)/);
6519
+ if (match) return `https://github.com/${match[1]}`;
6520
+ return null;
6521
+ }
6522
+ function resolveTestTarget(args) {
6523
+ const subs = getSubscriptions();
6524
+ if (subs.length === 0) {
6525
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6283
6526
  process.exit(1);
6284
6527
  }
6285
- const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
6286
- if (configInfo.proxies > cleanThreshold) {
6287
- console.log("");
6288
- console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${cleanThreshold}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
6289
- console.log("");
6290
- await sleep(1e3);
6291
- const progress = createProgressPrinter(rounds);
6292
- const cleanResult = await autoCleanSubscription(sub.name, {
6293
- timeout,
6294
- concurrency,
6295
- rounds,
6296
- onResult: progress.onResult,
6297
- onRetryRound: progress.onRetryRound
6298
- });
6299
- progress.finish();
6300
- console.log(formatTestSummary(cleanResult.summary));
6301
- if (cleanResult.skipped) {
6302
- 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"));
6303
- } else if (cleanResult.removedProxies > 0) {
6304
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
6305
- console.log("");
6306
- console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
6307
- if (!daemonEnabled) handleStopResult(stop());
6308
- try {
6309
- configInfo = prepareConfigForStart(targetMode, sub.name);
6310
- const pid = await launchOrRestart(targetMode);
6311
- console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
6312
- } catch (e) {
6313
- console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6314
- process.exit(1);
6315
- }
6528
+ const nameArg = getNonFlagArg(args, 2);
6529
+ const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6530
+ const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
6531
+ let target;
6532
+ if (nameArg) {
6533
+ const matches = findSubscriptionFuzzy(subs, nameArg);
6534
+ target = pickSingleSubscription(matches, nameArg);
6535
+ } else {
6536
+ const activeSub = getActiveSubscription();
6537
+ if (!activeSub) {
6538
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
6539
+ process.exit(1);
6316
6540
  }
6541
+ target = activeSub;
6317
6542
  }
6318
- printStatus();
6543
+ return { target, timeout, concurrency };
6319
6544
  }
6320
-
6321
- // src/commands/overwrite.ts
6322
- function printOverwriteList() {
6323
- const info = listOverwriteFile();
6324
- const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
6325
- console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}`);
6326
- console.log(`${colors.gray("\u4F4D\u7F6E: ")}${info.dir}`);
6327
- console.log("");
6328
- if (info.files.length === 0) {
6329
- console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
6330
- console.log("");
6331
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path8.join(info.dir, "overwrite.yaml")}`);
6332
- console.log(` \u6216 ${path8.join(info.dir, "overwrite.dns.yaml")}`);
6333
- console.log("");
6334
- } else {
6335
- console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
6336
- console.log("");
6337
- info.files.forEach((f, i) => {
6338
- const num = i < 10 ? ` ${i}` : `${i}`;
6339
- console.log(` ${num}. ${f.name}`);
6340
- if (f.keys.length > 0) {
6341
- console.log(` ${colors.gray("\u5B57\u6BB5: ")}${f.keys.join(", ")}`);
6342
- }
6343
- });
6545
+ function printRestartHintIfRunning() {
6546
+ if (getRunningState().running) {
6547
+ console.log(colors.yellow("\u63D0\u793A: \u8FD0\u884C\u4E2D\u7684\u5B9E\u4F8B\u4ECD\u4F7F\u7528\u65E7\u914D\u7F6E\uFF0C\u6267\u884C mihomo start \u4F7F\u66F4\u65B0\u751F\u6548"));
6344
6548
  console.log("");
6345
6549
  }
6346
- console.log("\u542F\u7528\u8986\u5199: mihomo ow on");
6347
- console.log("\u7981\u7528\u8986\u5199: mihomo ow off");
6348
- console.log("");
6349
6550
  }
6350
- async function cmdOverwrite(args) {
6351
- const action = args?.[1];
6352
- const currentMode = getRuntimeMode();
6353
- const restartNeeded = isRestartNeededOnChange();
6354
- if (action === "on" || action === "enable") {
6355
- if (isOverwriteEnabled()) {
6356
- console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u542F\u7528\u72B6\u6001");
6357
- console.log("");
6358
- printOverwriteList();
6359
- return;
6360
- }
6361
- setOverwriteEnabled(true);
6362
- console.log("\u5DF2\u542F\u7528\u8986\u5199\u914D\u7F6E");
6363
- if (restartNeeded) {
6364
- console.log("");
6365
- await cmdStart(["start", currentMode]);
6366
- return;
6367
- }
6551
+ function printSubscriptionList() {
6552
+ const subs = getSubscriptionsWithCache();
6553
+ if (subs.length === 0) {
6554
+ console.log("\u6CA1\u6709\u8BA2\u9605");
6555
+ console.log("");
6556
+ console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
6368
6557
  console.log("");
6369
- printOverwriteList();
6370
6558
  return;
6371
6559
  }
6372
- if (action === "off" || action === "disable") {
6373
- if (!isOverwriteEnabled()) {
6374
- console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u7981\u7528\u72B6\u6001");
6375
- console.log("");
6376
- printOverwriteList();
6377
- return;
6560
+ const activeSub = getActiveSubscription();
6561
+ console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
6562
+ subs.forEach((s, i) => {
6563
+ const time = formatDate(s.updated_at);
6564
+ const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
6565
+ const mergeBadge = isMultiUrl(s.url) ? colors.cyan(` [\u5408\u5E76 ${splitUrls(s.url).length} \u6E90]`) : "";
6566
+ const interval = resolveUpdateInterval(s.url, s.update_interval);
6567
+ console.log(` ${i + 1}. ${s.name}${defaultMark}${mergeBadge}`);
6568
+ console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
6569
+ if (s.username) {
6570
+ console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
6378
6571
  }
6379
- setOverwriteEnabled(false);
6380
- console.log("\u5DF2\u7981\u7528\u8986\u5199\u914D\u7F6E");
6381
- if (restartNeeded) {
6382
- console.log("");
6383
- await cmdStart(["start", currentMode]);
6384
- return;
6572
+ if (s.download !== void 0 || s.total !== void 0) {
6573
+ const used = (s.upload || 0) + (s.download || 0);
6574
+ const usedStr = formatBytes(used);
6575
+ const totalStr = formatBytes(s.total);
6576
+ let percentStr = "";
6577
+ if (s.total && s.total > 0) {
6578
+ const percent = Math.min(used / s.total * 100, 100);
6579
+ percentStr = ` (${percent.toFixed(1)}%)`;
6580
+ }
6581
+ console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
6385
6582
  }
6386
- console.log("");
6387
- printOverwriteList();
6388
- return;
6389
- }
6583
+ if (s.expire !== void 0) {
6584
+ console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
6585
+ }
6586
+ if (s.web_page_url) {
6587
+ console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
6588
+ }
6589
+ });
6590
+ console.log("");
6591
+ console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
6592
+ console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
6593
+ console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
6594
+ console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
6595
+ console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
6596
+ console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
6597
+ console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
6390
6598
  console.log("");
6391
- printOverwriteList();
6392
6599
  }
6393
-
6394
- // src/commands/reset.ts
6395
- import fs9 from "fs";
6396
- import readline from "readline";
6397
- var RESET_TARGETS = [
6398
- {
6399
- id: "subs",
6400
- aliases: ["sub", "subs", "subscription", "subscriptions"],
6401
- label: "\u8BA2\u9605",
6402
- paths: () => [DIRS.subscriptions],
6403
- needsStop: true
6404
- },
6405
- {
6406
- id: "logs",
6407
- aliases: ["log", "logs"],
6408
- label: "\u65E5\u5FD7",
6409
- paths: () => [DIRS.logs],
6410
- needsStop: false
6411
- },
6412
- {
6413
- id: "data",
6414
- aliases: ["data"],
6415
- label: "\u8FD0\u884C\u6570\u636E",
6416
- paths: () => [DIRS.data],
6417
- needsStop: true
6418
- },
6419
- {
6420
- id: "runtime",
6421
- aliases: ["runtime"],
6422
- label: "\u8FD0\u884C\u65F6",
6423
- paths: () => [DIRS.runtime],
6424
- needsStop: true
6425
- },
6426
- {
6427
- id: "settings",
6428
- aliases: ["setting", "settings", "config"],
6429
- label: "\u8BBE\u7F6E",
6430
- paths: () => [PATHS.settingsFile],
6431
- needsStop: false
6432
- },
6433
- {
6434
- id: "kernel",
6435
- aliases: ["kernel", "core"],
6436
- label: "\u5185\u6838",
6437
- paths: () => [DIRS.kernel],
6438
- needsStop: false,
6439
- onAfter: () => clearKernelVersionCache(),
6440
- checkEmpty: () => !hasKernel(),
6441
- emptyMsg: "\u5185\u6838\u672A\u5B89\u88C5\uFF0C\u65E0\u9700\u5220\u9664",
6442
- warnIfRunning: true
6443
- },
6444
- {
6445
- id: "overwrites",
6446
- aliases: ["overwrite", "overwrites", "ow"],
6447
- label: "\u8986\u5199",
6448
- paths: () => {
6449
- const dir = USER_DATA_DIR;
6450
- if (!fs9.existsSync(dir)) return [];
6451
- return fs9.readdirSync(dir).filter(isOverwriteFilename).map((f) => `${dir}/${f}`);
6452
- },
6453
- needsStop: false
6454
- },
6455
- {
6456
- id: "daemon",
6457
- aliases: ["daemon"],
6458
- label: "\u4FDD\u6D3B",
6459
- // 卸载由确认后的 disablesDaemon 段统一处理(需 sudo,受取消保护);
6460
- // 此处 paths 返回空(plist 在系统目录,用户态删不掉,且不应提前删破坏卸载),
6461
- // onAfter 因幂等守卫(plist 已删)成为 no-op,仅作单独 reset 未走前段时的兜底。
6462
- paths: () => [],
6463
- needsStop: false,
6464
- onAfter: () => disableDaemon(),
6465
- checkEmpty: () => !isDaemonEnabled(),
6466
- emptyMsg: "\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u9700\u5220\u9664"
6600
+ async function cmdSubscription(args) {
6601
+ const action = args[1];
6602
+ if (!action || action === "list") {
6603
+ printSubscriptionList();
6604
+ return;
6467
6605
  }
6468
- ];
6469
- function resolveResetTargets(names) {
6470
- const matched = [];
6471
- const unmatched = [];
6472
- for (const name of names) {
6473
- const t = RESET_TARGETS.find((t2) => t2.aliases.includes(name.toLowerCase()));
6474
- if (t) {
6475
- if (!matched.find((m) => m.id === t.id)) matched.push(t);
6606
+ if (action === "add") {
6607
+ const url = args[2]?.trim();
6608
+ const name = args[3] || "default";
6609
+ if (!url) {
6610
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6611
+ process.exit(1);
6612
+ }
6613
+ if (isMultiUrl(url)) {
6614
+ const urls = splitUrls(url);
6615
+ if (urls.length === 0) {
6616
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6617
+ process.exit(1);
6618
+ }
6619
+ for (const u of urls) {
6620
+ if (!isValidHttpUrl(u)) {
6621
+ console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
6622
+ process.exit(1);
6623
+ }
6624
+ }
6625
+ const normalizedUrl = urls.join(",");
6626
+ console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
6627
+ try {
6628
+ addSubscription(normalizedUrl, name);
6629
+ setDefaultSubscription(name);
6630
+ const info = await downloadMergedSubscription(urls, name);
6631
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
6632
+ } catch (e) {
6633
+ removeSubscription(name);
6634
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
6635
+ process.exit(1);
6636
+ }
6476
6637
  } else {
6477
- unmatched.push(name);
6638
+ if (!isValidHttpUrl(url)) {
6639
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL\uFF08\u9700\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF09");
6640
+ process.exit(1);
6641
+ }
6642
+ console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
6643
+ try {
6644
+ addSubscription(url, name);
6645
+ setDefaultSubscription(name);
6646
+ const info = await downloadSubscription(url, name);
6647
+ const repoUrl = githubRepoUrl(url);
6648
+ if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
6649
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
6650
+ } catch (e) {
6651
+ removeSubscription(name);
6652
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
6653
+ process.exit(1);
6654
+ }
6478
6655
  }
6656
+ console.log("");
6657
+ printSubscriptionList();
6658
+ return;
6479
6659
  }
6480
- return { matched, unmatched };
6481
- }
6482
- async function confirmPrompt(question) {
6483
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6484
- const answer = await new Promise((resolve) => {
6485
- rl.question(`${question} (y/N) `, (a) => {
6486
- rl.close();
6487
- resolve(a);
6488
- });
6489
- });
6490
- return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
6491
- }
6492
- async function cmdReset(args) {
6493
- const flags = (args || []).filter((a) => a.startsWith("-"));
6494
- const names = (args || []).slice(1).filter((a) => !a.startsWith("-"));
6495
- const fullReset = flags.includes("--full") || flags.includes("-f");
6496
- const skipConfirm = flags.includes("--yes") || flags.includes("-y");
6497
- let targets;
6498
- if (fullReset) {
6499
- targets = RESET_TARGETS;
6500
- } else if (names.length > 0) {
6501
- const { matched, unmatched } = resolveResetTargets(names);
6502
- if (unmatched.length > 0) {
6503
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u91CD\u7F6E\u76EE\u6807: ${unmatched.join(", ")}`);
6504
- console.log("");
6505
- console.log(`\u53EF\u7528\u76EE\u6807: ${RESET_TARGETS.map((t) => t.aliases[0]).join(", ")}`);
6660
+ if (action === "update") {
6661
+ const name = args[2];
6662
+ const subs = getSubscriptions();
6663
+ if (subs.length === 0) {
6664
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6665
+ process.exit(1);
6666
+ }
6667
+ if (!name) {
6668
+ console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
6669
+ const results = await Promise.all(subs.map((sub) => tryUpdateOne(sub)));
6670
+ let ok = 0;
6671
+ for (const r of results) {
6672
+ if (r.success) ok++;
6673
+ printUpdateResult(r);
6674
+ }
6675
+ if (ok === 0) process.exit(1);
6506
6676
  console.log("");
6507
- console.log("\u793A\u4F8B:");
6508
- console.log(" mihomo reset sub log # \u5220\u9664\u8BA2\u9605\u548C\u65E5\u5FD7");
6509
- console.log(" mihomo reset kernel # \u53EA\u5220\u5185\u6838");
6510
- console.log(" mihomo reset --full # \u5220\u9664\u5168\u90E8");
6511
- console.log(" mihomo reset # \u5220\u9664\u5168\u90E8\uFF08\u4FDD\u7559\u8BBE\u7F6E\u3001\u5185\u6838\u3001\u8986\u5199\uFF09");
6677
+ printRestartHintIfRunning();
6678
+ printSubscriptionList();
6679
+ return;
6680
+ }
6681
+ const matches = findSubscriptionFuzzy(subs, name);
6682
+ const target = pickSingleSubscription(matches, name);
6683
+ console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
6684
+ const result = await tryUpdateOne(target);
6685
+ if (!result.success) {
6686
+ console.error(`\u66F4\u65B0\u5931\u8D25: ${(result.error || "").split("\n")[0]}`);
6512
6687
  process.exit(1);
6513
6688
  }
6514
- targets = matched;
6515
- } else {
6516
- targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
6689
+ console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(result)})`);
6690
+ console.log("");
6691
+ printRestartHintIfRunning();
6692
+ printSubscriptionList();
6693
+ return;
6517
6694
  }
6518
- for (const t of targets) {
6519
- if (t.checkEmpty?.()) {
6520
- if (targets.length === 1) {
6521
- console.log(t.emptyMsg);
6522
- return;
6695
+ if (action === "use") {
6696
+ const name = args[2];
6697
+ const subs = getSubscriptions();
6698
+ if (!name) {
6699
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
6700
+ if (subs.length > 0) {
6701
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
6702
+ for (const s of subs) console.log(` ${s.name}`);
6523
6703
  }
6704
+ process.exit(1);
6524
6705
  }
6525
- }
6526
- const needsStop = targets.some((t) => t.needsStop);
6527
- const warnRunning = targets.some((t) => t.warnIfRunning);
6528
- const kernelTargeted = targets.some((t) => t.id === "kernel");
6529
- const daemonTargeted = targets.some((t) => t.id === "daemon");
6530
- const disablesDaemon = needsStop || kernelTargeted || daemonTargeted;
6531
- const pids = needsStop || warnRunning ? getMihomoPids() : [];
6532
- if (warnRunning && pids.length > 0) {
6533
- console.log(colors.yellow(`\u8B66\u544A: mihomo \u6B63\u5728\u8FD0\u884C (PID ${pids.join(", ")})\uFF0C\u5220\u9664\u5185\u6838\u540E\u5C06\u65E0\u6CD5\u91CD\u65B0\u542F\u52A8`));
6534
- }
6535
- if (disablesDaemon && isDaemonEnabled()) {
6536
- console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u91CD\u7F6E\u5C06\u4E00\u5E76\u5173\u95ED\u4FDD\u6D3B\uFF08\u79FB\u9664\u5F00\u673A\u81EA\u542F\uFF09"));
6537
- }
6538
- console.log(`\u5C06\u5220\u9664: ${targets.map((t) => t.label).join("\u3001")}`);
6539
- if (!skipConfirm && !await confirmPrompt("\u786E\u8BA4?")) {
6540
- console.log("\u5DF2\u53D6\u6D88");
6706
+ const matches = findSubscriptionFuzzy(subs, name);
6707
+ const target = pickSingleSubscription(matches, name);
6708
+ const currentDefault = getActiveSubscription();
6709
+ const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
6710
+ if (isAlreadyDefault) {
6711
+ console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
6712
+ console.log("");
6713
+ printSubscriptionList();
6714
+ return;
6715
+ }
6716
+ const currentMode = getRuntimeMode();
6717
+ const restartNeeded = isRestartNeededOnChange();
6718
+ const success = setDefaultSubscription(target.name);
6719
+ if (success) {
6720
+ console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
6721
+ } else {
6722
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
6723
+ process.exit(1);
6724
+ }
6725
+ if (restartNeeded) {
6726
+ console.log("");
6727
+ await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
6728
+ return;
6729
+ }
6730
+ console.log("");
6731
+ printSubscriptionList();
6541
6732
  return;
6542
6733
  }
6543
- if (disablesDaemon && isDaemonEnabled()) {
6544
- try {
6545
- disableDaemon();
6546
- } catch (e) {
6547
- console.error(`${colors.red("\u4FDD\u6D3B\u5173\u95ED\u5DF2\u53D6\u6D88\uFF0C\u91CD\u7F6E\u4E2D\u6B62:")} ${e.message.split("\n")[0]}`);
6548
- return;
6734
+ if (action === "web" || action === "open") {
6735
+ const name = args[2];
6736
+ const subs = getSubscriptionsWithCache();
6737
+ if (subs.length === 0) {
6738
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6739
+ process.exit(1);
6740
+ }
6741
+ let target;
6742
+ if (name) {
6743
+ const matches = findSubscriptionFuzzy(subs, name);
6744
+ target = pickSingleSubscription(matches, name);
6745
+ } else {
6746
+ target = getActiveSubscription() || subs[0];
6747
+ }
6748
+ const cached = subs.find((s) => s.name === target.name);
6749
+ let webPageUrl = cached?.web_page_url;
6750
+ if (!webPageUrl) {
6751
+ console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u67E5\u8BE2\u8BA2\u9605...");
6752
+ try {
6753
+ const info = isMultiUrl(target.url) ? await downloadMergedSubscription(splitUrls(target.url), target.name, void 0, false) : await downloadSubscription(target.url, target.name, void 0, false);
6754
+ if (info.webPageUrl) {
6755
+ webPageUrl = info.webPageUrl;
6756
+ } else {
6757
+ console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
6758
+ process.exit(1);
6759
+ }
6760
+ } catch (e) {
6761
+ console.error(`\u67E5\u8BE2\u5931\u8D25: ${e.message}`);
6762
+ process.exit(1);
6763
+ }
6764
+ }
6765
+ console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
6766
+ const opened = openUrl(webPageUrl);
6767
+ if (!opened) {
6768
+ console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
6549
6769
  }
6770
+ return;
6550
6771
  }
6551
- if (needsStop && getMihomoPids().length > 0) {
6552
- console.log("\u505C\u6B62\u8FDB\u7A0B...");
6553
- cleanupAll();
6554
- for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6555
- if (getMihomoPids().length === 0) break;
6556
- await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6772
+ if (action === "remove" || action === "rm" || action === "delete") {
6773
+ const name = args[2];
6774
+ const subs = getSubscriptions();
6775
+ if (!name) {
6776
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
6777
+ if (subs.length > 0) {
6778
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
6779
+ for (const s of subs) console.log(` ${s.name}`);
6780
+ }
6781
+ process.exit(1);
6782
+ }
6783
+ const matches = findSubscriptionFuzzy(subs, name);
6784
+ const target = pickSingleSubscription(matches, name);
6785
+ const switchedTo = removeSubscription(target.name);
6786
+ console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
6787
+ if (switchedTo) {
6788
+ console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
6557
6789
  }
6790
+ console.log("");
6791
+ printSubscriptionList();
6792
+ return;
6558
6793
  }
6559
- for (const t of targets) {
6560
- for (const p of t.paths()) {
6561
- if (fs9.existsSync(p)) {
6562
- try {
6563
- rmrf(p);
6564
- } catch (e) {
6565
- console.warn(` \u8B66\u544A: \u65E0\u6CD5\u5220\u9664 ${p}: ${e.message}`);
6566
- }
6794
+ if (action === "clean") {
6795
+ const { target, timeout, concurrency } = resolveTestTarget(args);
6796
+ const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6797
+ console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
6798
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6799
+ console.log("");
6800
+ const progress = createProgressPrinter(rounds);
6801
+ const result = await withTestInstance(target.name, async (apiBase) => {
6802
+ return autoCleanSubscription(target.name, {
6803
+ timeout,
6804
+ concurrency,
6805
+ rounds,
6806
+ apiBase,
6807
+ onResult: progress.onResult,
6808
+ onRetryRound: progress.onRetryRound
6809
+ });
6810
+ });
6811
+ progress.finish();
6812
+ console.log(formatTestSummary(result.summary));
6813
+ if (result.skipped) {
6814
+ console.log("");
6815
+ 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"));
6816
+ } else if (result.removedProxies > 0) {
6817
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6818
+ if (getRunningState().running) {
6819
+ console.log("");
6820
+ console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
6567
6821
  }
6568
6822
  }
6569
- t.onAfter?.();
6823
+ return;
6570
6824
  }
6571
- ensureDirs();
6572
- if (targets.some((t) => t.id === "settings")) {
6573
- invalidateSettingsCache();
6825
+ if (action === "test") {
6826
+ const { target, timeout, concurrency } = resolveTestTarget(args);
6827
+ console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
6828
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6829
+ console.log("");
6830
+ const progress = createProgressPrinter();
6831
+ const summary = await withTestInstance(target.name, async (apiBase) => {
6832
+ return testSubscriptionProxies(target.name, {
6833
+ timeout,
6834
+ concurrency,
6835
+ apiBase,
6836
+ onResult: progress.onResult
6837
+ });
6838
+ });
6839
+ progress.finish();
6840
+ console.log(formatTestSummary(summary));
6841
+ return;
6574
6842
  }
6575
- console.log(colors.green(`\u5DF2\u91CD\u7F6E: ${targets.map((t) => t.label).join("\u3001")}`));
6843
+ console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
6844
+ console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
6845
+ process.exit(1);
6576
6846
  }
6577
6847
 
6578
6848
  // src/commands/test.ts
@@ -6640,7 +6910,14 @@ async function cmdClean(args) {
6640
6910
  const mode = getRuntimeMode();
6641
6911
  const daemonManaged = isDaemonEnabled();
6642
6912
  try {
6643
- if (!daemonManaged) handleStopResult(stop());
6913
+ if (!daemonManaged) {
6914
+ if (hasRootResidue()) {
6915
+ console.error(`${colors.red("\u9519\u8BEF:")} \u4E3B\u5B9E\u4F8B\u4EE5 root \u8FD0\u884C\uFF08TUN\uFF09\uFF0C\u505C\u6B62\u5B83\u9700\u8981 sudo`);
6916
+ console.error("\u8BF7\u6539\u7528 mihomo sub clean\uFF08\u9694\u79BB\u5B9E\u4F8B\u6D4B\u901F\uFF0C\u65E0\u9700\u505C\u6B62\u4E3B\u5B9E\u4F8B\uFF09");
6917
+ process.exit(1);
6918
+ }
6919
+ handleStopResult(stop());
6920
+ }
6644
6921
  const configInfo = prepareConfigForStart(mode, activeSub.name);
6645
6922
  const pid = await launchOrRestart(mode);
6646
6923
  const label = daemonManaged ? "\u5DF2\u91CD\u542F (\u4FDD\u6D3B)" : "\u5DF2\u91CD\u542F";
@@ -6663,6 +6940,10 @@ function cmdUI(args) {
6663
6940
  const url = UI_URLS[uiName];
6664
6941
  console.log(`\u6253\u5F00 Web UI: ${uiName}`);
6665
6942
  console.log(`\u5730\u5740: ${url}`);
6943
+ const secret = readSettings().controller_secret;
6944
+ if (secret) {
6945
+ console.log("\u5DF2\u914D\u7F6E\u8BBF\u95EE\u5BC6\u94A5\uFF08UI \u8FDE\u63A5 127.0.0.1:9090 \u65F6\u9700\u8F93\u5165\uFF0C\u5BC6\u94A5\u89C1 settings.json\uFF09");
6946
+ }
6666
6947
  const success = openUrl(url);
6667
6948
  if (!success) {
6668
6949
  console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
@@ -6684,11 +6965,16 @@ async function cmdUpdate() {
6684
6965
  if (code === 0) {
6685
6966
  resolve();
6686
6967
  } else {
6968
+ console.error("\u66F4\u65B0\u5931\u8D25\u3002\u82E5\u4E3A\u6743\u9650\u95EE\u9898\uFF08EACCES\uFF09\uFF0C\u53EF\u5C1D\u8BD5: sudo npm install -g mihomo-cli");
6687
6969
  process.exit(code || 1);
6688
6970
  }
6689
6971
  });
6690
6972
  npm.on("error", (e) => {
6691
- console.error(`\u6267\u884C\u5931\u8D25: ${e.message}`);
6973
+ if (e.message.includes("EACCES") || e.message.includes("permission")) {
6974
+ console.error("\u6743\u9650\u4E0D\u8DB3\uFF0C\u53EF\u5C1D\u8BD5: sudo npm install -g mihomo-cli");
6975
+ } else {
6976
+ console.error(`\u6267\u884C\u5931\u8D25: ${e.message}`);
6977
+ }
6692
6978
  process.exit(1);
6693
6979
  });
6694
6980
  });
@@ -6716,7 +7002,7 @@ var COMMANDS = [
6716
7002
  aliases: ["up"],
6717
7003
  handler: cmdStart,
6718
7004
  group: "control",
6719
- usage: ["start [tun|mixed] [-s] [-u ms] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)", " [-r N] [-t ms] [-j N]"]
7005
+ usage: ["start [tun|mixed] [-s] [-u ms] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)", " [-r N] [-t ms] [-j N] [--no-clean]"]
6720
7006
  },
6721
7007
  {
6722
7008
  name: "tun",
@@ -6775,8 +7061,8 @@ var COMMANDS = [
6775
7061
  "subscription update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09",
6776
7062
  "subscription remove <name> \u5220\u9664\u8BA2\u9605",
6777
7063
  "subscription web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762",
6778
- "subscription test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027",
6779
- "subscription clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9"
7064
+ "subscription test [name] \u6D4B\u8BD5\u8282\u70B9\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u65E0\u9700\u8FD0\u884C\uFF09",
7065
+ "subscription clean [name] \u6D4B\u901F\u6E05\u7406\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u4E0D\u52A8\u4E3B\u5B9E\u4F8B\uFF09"
6780
7066
  ]
6781
7067
  },
6782
7068
  {
@@ -6792,14 +7078,14 @@ var COMMANDS = [
6792
7078
  aliases: [],
6793
7079
  handler: cmdTest,
6794
7080
  group: "subscription",
6795
- usage: ["test [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027"]
7081
+ usage: ["test [-t ms] [-j N] \u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\uFF08\u7ECF\u8FD0\u884C\u4E2D\u7684\u4E3B\u5B9E\u4F8B\uFF09"]
6796
7082
  },
6797
7083
  {
6798
7084
  name: "clean",
6799
7085
  aliases: [],
6800
7086
  handler: cmdClean,
6801
7087
  group: "subscription",
6802
- usage: ["clean [-t ms] [-j N] [-r N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F"]
7088
+ usage: ["clean [-t ms] [-j N] [-r N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u91CD\u542F\uFF08\u7ECF\u4E3B\u5B9E\u4F8B\uFF09"]
6803
7089
  },
6804
7090
  // === 配置 ===
6805
7091
  {
@@ -6867,7 +7153,7 @@ var COMMANDS = [
6867
7153
  aliases: [],
6868
7154
  handler: cmdReset,
6869
7155
  group: "system",
6870
- usage: ["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"]
7156
+ usage: ["reset [\u76EE\u6807...] [--full] [-y] \u91CD\u7F6E: \u7559\u7A7A\u4FDD\u7559\u8BBE\u7F6E/\u5185\u6838/\u8986\u5199, \u6307\u5B9A\u76EE\u6807\u5220\u5BF9\u5E94\u9879, --full \u5220\u5168\u90E8, -y \u8DF3\u8FC7\u786E\u8BA4"]
6871
7157
  },
6872
7158
  // === meta(不在分组清单展示,help 末尾单列) ===
6873
7159
  {
@@ -6903,7 +7189,9 @@ function findCommand(token) {
6903
7189
 
6904
7190
  // src/index.ts
6905
7191
  process.on("SIGINT", () => {
6906
- console.log("\n\u6B63\u5728\u9000\u51FA...");
7192
+ if (!isSilentSigint()) {
7193
+ console.log("\n\u6B63\u5728\u9000\u51FA...");
7194
+ }
6907
7195
  runCleanup();
6908
7196
  process.exit(130);
6909
7197
  });