164 lines
4.5 KiB
Python
164 lines
4.5 KiB
Python
#!/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",
|
|
)
|