feat: support Clash Verge global script on Windows

This commit is contained in:
2026-08-30 14:31:06 +08:00
parent de26a0edc0
commit f05bffd587
3 changed files with 87 additions and 8 deletions
+4 -2
View File
@@ -27,15 +27,17 @@ vpn-rules/
## 消费端接入 ## 消费端接入
### Clash Verge(macOS) ### Clash Verge(Windows / macOS)
```bash ```bash
# 生成并写入 Clash Verge 数据目录的 Script.js # 按当前平台生成并写入 Clash Verge 数据目录的全局 Script.js
python3 scripts/gen_clash_verge_script.py python3 scripts/gen_clash_verge_script.py
# 然后在 Clash Verge 界面点「重启内核」生效 # 然后在 Clash Verge 界面点「重启内核」生效
``` ```
对当前及以后新增/切换的所有订阅生效;订阅更新不会覆盖它。 对当前及以后新增/切换的所有订阅生效;订阅更新不会覆盖它。
Windows 默认写入 `%APPDATA%\io.github.clash-verge-rev.clash-verge-rev\profiles\Script.js`;
macOS 默认写入 `~/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/profiles/Script.js`。
### 路由器 mihomo(Magic Catling2 / OpenWrt) ### 路由器 mihomo(Magic Catling2 / OpenWrt)
+30 -5
View File
@@ -4,14 +4,39 @@
用法: 用法:
python3 scripts/gen_clash_verge_script.py [--out 目标路径] python3 scripts/gen_clash_verge_script.py [--out 目标路径]
默认输出到 Clash Verge 数据目录 profiles/Script.js(全局直连规则的唯一正确机制, 默认按 Windows/macOS 平台输出到 Clash Verge 数据目录 profiles/Script.js(全局直连
见 clash-verge-config 记忆)。生成后需在 Clash Verge 界面点「重启内核」生效。 规则的唯一正确机制)。生成后需在 Clash Verge 界面点「重启内核」生效。
""" """
import argparse import argparse
import os
from pathlib import Path from pathlib import Path
import sys
REPO_ROOT = Path(__file__).resolve().parent.parent REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_OUT = Path.home() / "Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/profiles/Script.js" APP_ID = "io.github.clash-verge-rev.clash-verge-rev"
def default_output_path(
platform_name: str = sys.platform,
*,
home: Path | None = None,
appdata: Path | None = None,
) -> Path:
"""返回当前平台的 Clash Verge 全局 Script.js 路径。"""
home = home or Path.home()
if platform_name == "win32":
if appdata is None:
roaming = os.environ.get("APPDATA")
if not roaming:
raise SystemExit("[error] Windows 环境缺少 APPDATA,无法定位 Clash Verge 数据目录")
appdata = Path(roaming)
return appdata / APP_ID / "profiles" / "Script.js"
if platform_name == "darwin":
return home / "Library/Application Support" / APP_ID / "profiles" / "Script.js"
return home / ".local/share" / APP_ID / "profiles" / "Script.js"
DEFAULT_OUT = default_output_path()
def load_payload(path: Path) -> list[str]: def load_payload(path: Path) -> list[str]:
@@ -22,7 +47,7 @@ def load_payload(path: Path) -> list[str]:
if line.startswith("- "): if line.startswith("- "):
payload.append(line[2:].strip()) payload.append(line[2:].strip())
if not payload: if not payload:
raise SystemExit(f"❌ direct.yaml 里没有 payload 条目: {path}") raise SystemExit(f"[error] direct.yaml 里没有 payload 条目: {path}")
return payload return payload
@@ -66,7 +91,7 @@ def main() -> None:
out = Path(args.out) out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True) out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(script, encoding="utf-8") out.write_text(script, encoding="utf-8")
print(f"✅ 已生成 {out}({len(payload)} 条规则)") print(f"[ok] 已生成 {out}({len(payload)} 条规则)")
print(" 请在 Clash Verge 界面点「重启内核」生效。") print(" 请在 Clash Verge 界面点「重启内核」生效。")
+53 -1
View File
@@ -1,7 +1,10 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import subprocess
import sys import sys
import tempfile
import unittest import unittest
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
@@ -10,10 +13,59 @@ ROOT = Path(__file__).resolve().parents[1]
SPEC_PATH = ROOT / "MODULE_SPEC.yaml" SPEC_PATH = ROOT / "MODULE_SPEC.yaml"
sys.path.insert(0, str(ROOT / "scripts")) sys.path.insert(0, str(ROOT / "scripts"))
from gen_clash_verge_script import load_payload, to_rule # noqa: E402 from gen_clash_verge_script import ( # noqa: E402
default_output_path,
load_payload,
to_rule,
)
class ModuleSpecTests(unittest.TestCase): class ModuleSpecTests(unittest.TestCase):
def test_clash_verge_default_output_path_is_platform_aware(self) -> None:
home = Path("C:/Users/example")
appdata = Path("C:/Users/example/AppData/Roaming")
self.assertEqual(
default_output_path("win32", home=home, appdata=appdata),
appdata
/ "io.github.clash-verge-rev.clash-verge-rev"
/ "profiles"
/ "Script.js",
)
self.assertEqual(
default_output_path("darwin", home=home),
home
/ "Library/Application Support"
/ "io.github.clash-verge-rev.clash-verge-rev"
/ "profiles"
/ "Script.js",
)
def test_clash_verge_generator_supports_windows_gbk_console(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
output = Path(temp_dir) / "Script.js"
env = os.environ.copy()
env["PYTHONIOENCODING"] = "gbk"
result = subprocess.run(
[
sys.executable,
str(ROOT / "scripts/gen_clash_verge_script.py"),
"--out",
str(output),
],
cwd=ROOT,
env=env,
capture_output=True,
check=False,
)
self.assertEqual(
result.returncode,
0,
result.stderr.decode("gbk", errors="replace"),
)
self.assertTrue(output.is_file())
def test_module_spec_declares_rule_authority_and_safe_context(self) -> None: def test_module_spec_declares_rule_authority_and_safe_context(self) -> None:
spec = json.loads(SPEC_PATH.read_text(encoding="utf-8")) spec = json.loads(SPEC_PATH.read_text(encoding="utf-8"))