#!/usr/bin/env python3 from __future__ import annotations import json from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SURGE_DIR = ROOT / "rule" / "Surge" LOON_DIR = ROOT / "rule" / "Loon" CLASH_DIR = ROOT / "rule" / "Clash" SINGBOX_DIR = ROOT / "rule" / "sing-box" LOON_DIR.mkdir(parents=True, exist_ok=True) CLASH_DIR.mkdir(parents=True, exist_ok=True) SINGBOX_DIR.mkdir(parents=True, exist_ok=True) UPSTREAM_TARGETS = { "Lan": "Lan.list", "Apple": "Apple.list", "OpenAI": "OpenAI.list", "Gemini": "Gemini.list", "Claude": "Claude.list", "China": "China.list", "ChinaIPs": "ChinaIPs.list", "Proxy": "Proxy.list", } FIELD_MAP = { "DOMAIN": "domain", "DOMAIN-SUFFIX": "domain_suffix", "DOMAIN-KEYWORD": "domain_keyword", "DOMAIN-REGEX": "domain_regex", "IP-CIDR": "ip_cidr", "IP-CIDR6": "ip_cidr", "PROCESS-NAME": "process_name", } LOON_SUPPORTED_RULES = { "DOMAIN-SUFFIX", "DOMAIN", "DOMAIN-KEYWORD", "USER-AGENT", "URL-REGEX", "IP-CIDR", "GEOIP", "FINAL", } CLASH_SUPPORTED_RULES = { "DOMAIN-SUFFIX", "DOMAIN-KEYWORD", "DOMAIN", "SRC-IP-CIDR", "IP-CIDR", "IP-CIDR6", "GEOIP", "DST-PORT", "SRC-PORT", "MATCH", "FINAL", "PROCESS-NAME", } def build_rule_set(entries: dict[str, list[str]]) -> dict: rules = [] for field in ("domain", "domain_suffix", "domain_keyword", "domain_regex", "ip_cidr", "process_name"): values = entries.get(field, []) if values: rules.append({field: values}) return {"version": 3, "rules": rules} def parse_surge_list(path: Path) -> dict: entries: dict[str, list[str]] = {} for raw in path.read_text(encoding="utf-8").splitlines(): line = raw.strip() if not line or line.startswith("#"): continue parts = [part.strip() for part in line.split(",")] if len(parts) < 2: continue kind = parts[0].upper() field = FIELD_MAP.get(kind) if not field: continue value = parts[1] if not value: continue entries.setdefault(field, []) if value not in entries[field]: entries[field].append(value) return build_rule_set(entries) def convert_for_target(path: Path, target_rules: set[str]) -> list[str]: out: list[str] = [] for raw in path.read_text(encoding="utf-8").splitlines(): line = raw.rstrip() stripped = line.strip() if not stripped or stripped.startswith("#"): out.append(line) continue parts = [part.strip() for part in stripped.split(",")] if len(parts) < 2: continue kind = parts[0].upper() if kind not in target_rules: continue if kind == "FINAL": if target_rules is CLASH_SUPPORTED_RULES: out.append(line.replace("FINAL,", "MATCH,", 1)) else: out.append(line) continue out.append(line) return out MANUAL_RULESETS = { "ManualBackHome": {"version": 3, "rules": [{"ip_cidr": ["192.168.10.0/24"]}]}, "ManualReject": {"version": 3, "rules": [{"domain": ["www.axure.com"]}]}, "ManualAI": {"version": 3, "rules": [{"domain_keyword": ["macked"]}]}, "ManualDirect": { "version": 3, "rules": [ { "domain_suffix": [ "umeng.com", "umsns.com", "umindex.com", "nice.com", "apple.com", "alicdn.com", "qujiangkeji.com", "banxueketang.com", "doubj.cn", "local", ] } ], }, } for name, filename in UPSTREAM_TARGETS.items(): data = parse_surge_list(SURGE_DIR / filename) (SINGBOX_DIR / f"{name}.json").write_text( json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) loon_lines = convert_for_target(SURGE_DIR / filename, LOON_SUPPORTED_RULES) (LOON_DIR / filename).write_text("\n".join(loon_lines).rstrip() + "\n", encoding="utf-8") clash_lines = convert_for_target(SURGE_DIR / filename, CLASH_SUPPORTED_RULES) (CLASH_DIR / filename).write_text("\n".join(clash_lines).rstrip() + "\n", encoding="utf-8") for name, payload in MANUAL_RULESETS.items(): (SINGBOX_DIR / f"{name}.json").write_text( json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", )