#!/usr/bin/env python3 """把 direct.yaml(真源)生成为 Clash Verge 的全局增强脚本 Script.js。 用法: python3 scripts/gen_clash_verge_script.py [--out 目标路径] 默认按 Windows/macOS 平台输出到 Clash Verge 数据目录 profiles/Script.js(全局直连 规则的唯一正确机制)。生成后需在 Clash Verge 界面点「重启内核」生效。 """ import argparse import os from pathlib import Path import sys REPO_ROOT = Path(__file__).resolve().parent.parent 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]: payload: list[str] = [] with path.open(encoding="utf-8") as f: for line in f: line = line.strip() if line.startswith("- "): payload.append(line[2:].strip()) if not payload: raise SystemExit(f"[error] direct.yaml 里没有 payload 条目: {path}") return payload def to_rule(entry: str) -> str: """rule-provider 条目(无策略)→ 完整规则(补 DIRECT)。""" if entry.endswith(",no-resolve"): return f"{entry[:-len(',no-resolve')]},DIRECT,no-resolve" return f"{entry},DIRECT" def render_script(payload: list[str]) -> str: rules = [f" {to_rule(e)!r}," for e in payload] lines = "\n".join(rules) return f"""// 由 vpn-rules/direct.yaml 生成 — 勿手改,改真源后重跑 gen_clash_verge_script.py。 // 全局直连规则:OneDrive、FRP 控制面、B站、国内 LLM、内网段、国区 iCloud、UGREEN 一律 DIRECT。 const prependRules = [ {lines} ]; // 国内可达的 DoH 同时作为 fallback,确保 OneDrive 等境外域名在 DIRECT 出站前可解析。 const directDnsFallbacks = [ 'https://dns.alidns.com/dns-query', 'https://doh.pub/dns-query', ]; function main(config, profileName) {{ const existingRules = Array.isArray(config.rules) ? config.rules : []; const existingRuleSet = new Set(existingRules); const dns = config.dns && typeof config.dns === 'object' && !Array.isArray(config.dns) ? config.dns : {{}}; const existingFallbacks = Array.isArray(dns.fallback) ? dns.fallback : (typeof dns.fallback === 'string' ? [dns.fallback] : []); const existingFallbackSet = new Set(existingFallbacks); config.rules = [ ...prependRules.filter((rule) => !existingRuleSet.has(rule)), ...existingRules, ]; dns.fallback = [ ...directDnsFallbacks.filter((server) => !existingFallbackSet.has(server)), ...existingFallbacks, ]; config.dns = dns; return config; }} """ def main() -> None: ap = argparse.ArgumentParser(description="direct.yaml → Clash Verge Script.js") ap.add_argument("--out", default=str(DEFAULT_OUT), help="输出路径") args = ap.parse_args() payload = load_payload(REPO_ROOT / "direct.yaml") script = render_script(payload) out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) out.write_text(script, encoding="utf-8") print(f"[ok] 已生成 {out}({len(payload)} 条规则)") print(" 请在 Clash Verge 界面点「重启内核」生效。") if __name__ == "__main__": main()