feat: add config expander and Mihomo config builder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import yaml
|
||||
|
||||
PROXY_FIELDS = {
|
||||
"name", "type", "server", "port", "uuid", "password", "cipher",
|
||||
"alterId", "tls", "network", "ws-opts", "h2-opts", "http-opts",
|
||||
"grpc-opts", "reality-opts", "servername", "fingerprint", "flow",
|
||||
"udp", "skip-cert-verify", "sni", "username", "plugin", "plugin-opts",
|
||||
"client-fingerprint", "early-data-header-name", "smux",
|
||||
}
|
||||
|
||||
|
||||
def filter_proxy(proxy: dict) -> dict:
|
||||
return {k: v for k, v in proxy.items() if k in PROXY_FIELDS}
|
||||
|
||||
|
||||
def expand_config(base_yaml: str, provider_proxies: dict[str, list[dict]]) -> str:
|
||||
cfg = yaml.safe_load(base_yaml)
|
||||
|
||||
all_proxies = list(cfg.get("proxies") or [])
|
||||
provider_to_names: dict[str, list[str]] = {}
|
||||
|
||||
for provider_name, proxies in provider_proxies.items():
|
||||
filtered = [filter_proxy(p) for p in proxies]
|
||||
provider_to_names[provider_name] = [p["name"] for p in filtered]
|
||||
all_proxies.extend(filtered)
|
||||
|
||||
cfg["proxies"] = all_proxies
|
||||
|
||||
for group in cfg.get("proxy-groups") or []:
|
||||
if "use" in group:
|
||||
expanded: list[str] = []
|
||||
for pname in group.pop("use"):
|
||||
expanded.extend(provider_to_names.get(pname, []))
|
||||
group.setdefault("proxies", [])
|
||||
group["proxies"] = group["proxies"] + expanded
|
||||
|
||||
cfg.pop("proxy-providers", None)
|
||||
|
||||
return yaml.dump(cfg, allow_unicode=True, sort_keys=False)
|
||||
|
||||
|
||||
def build_mihomo_config(all_base_yamls: list[str], secret: str) -> str:
|
||||
providers: dict[str, dict] = {}
|
||||
|
||||
for base_yaml in all_base_yamls:
|
||||
try:
|
||||
cfg = yaml.safe_load(base_yaml) or {}
|
||||
for name, provider in (cfg.get("proxy-providers") or {}).items():
|
||||
if name not in providers:
|
||||
providers[name] = provider
|
||||
except yaml.YAMLError:
|
||||
continue
|
||||
|
||||
config: dict = {
|
||||
"external-controller": "0.0.0.0:9090",
|
||||
"secret": secret,
|
||||
"mixed-port": 7890,
|
||||
"allow-lan": False,
|
||||
}
|
||||
if providers:
|
||||
config["proxy-providers"] = providers
|
||||
|
||||
return yaml.dump(config, allow_unicode=True, sort_keys=False)
|
||||
@@ -0,0 +1,151 @@
|
||||
import yaml
|
||||
from expander import filter_proxy, expand_config, build_mihomo_config
|
||||
|
||||
PROXY_FULL = {
|
||||
"name": "node1",
|
||||
"type": "ss",
|
||||
"server": "1.2.3.4",
|
||||
"port": 443,
|
||||
"password": "secret",
|
||||
"cipher": "aes-256-gcm",
|
||||
"alive": True, # runtime field — should be stripped
|
||||
"history": [], # runtime field — should be stripped
|
||||
"extra": {"foo": "bar"}, # runtime field — should be stripped
|
||||
}
|
||||
|
||||
BASE_YAML = """
|
||||
proxies: []
|
||||
proxy-providers:
|
||||
provider1:
|
||||
type: http
|
||||
url: https://example.com/sub
|
||||
interval: 3600
|
||||
proxy-groups:
|
||||
- name: Proxy
|
||||
type: select
|
||||
use:
|
||||
- provider1
|
||||
rules:
|
||||
- MATCH,DIRECT
|
||||
"""
|
||||
|
||||
|
||||
def test_filter_proxy_removes_runtime_fields():
|
||||
filtered = filter_proxy(PROXY_FULL)
|
||||
assert "alive" not in filtered
|
||||
assert "history" not in filtered
|
||||
assert "extra" not in filtered
|
||||
assert filtered["name"] == "node1"
|
||||
assert filtered["server"] == "1.2.3.4"
|
||||
|
||||
|
||||
def test_expand_config_replaces_providers_with_proxies():
|
||||
proxies = [
|
||||
{"name": "node1", "type": "ss", "server": "1.2.3.4", "port": 443,
|
||||
"password": "pwd", "cipher": "aes-256-gcm"},
|
||||
]
|
||||
result = expand_config(BASE_YAML, {"provider1": proxies})
|
||||
cfg = yaml.safe_load(result)
|
||||
|
||||
assert "proxy-providers" not in cfg
|
||||
assert any(p["name"] == "node1" for p in cfg["proxies"])
|
||||
|
||||
|
||||
def test_expand_config_fills_use_in_proxy_groups():
|
||||
proxies = [
|
||||
{"name": "node1", "type": "ss", "server": "1.2.3.4", "port": 443,
|
||||
"password": "pwd", "cipher": "aes-256-gcm"},
|
||||
{"name": "node2", "type": "ss", "server": "5.6.7.8", "port": 443,
|
||||
"password": "pwd", "cipher": "aes-256-gcm"},
|
||||
]
|
||||
result = expand_config(BASE_YAML, {"provider1": proxies})
|
||||
cfg = yaml.safe_load(result)
|
||||
|
||||
proxy_group = next(g for g in cfg["proxy-groups"] if g["name"] == "Proxy")
|
||||
assert "use" not in proxy_group
|
||||
assert "node1" in proxy_group["proxies"]
|
||||
assert "node2" in proxy_group["proxies"]
|
||||
|
||||
|
||||
def test_expand_config_preserves_existing_proxies():
|
||||
base = """
|
||||
proxies:
|
||||
- name: static-node
|
||||
type: direct
|
||||
proxy-providers:
|
||||
p1:
|
||||
type: http
|
||||
url: https://example.com/sub
|
||||
interval: 3600
|
||||
proxy-groups: []
|
||||
rules: []
|
||||
"""
|
||||
result = expand_config(base, {"p1": [{"name": "dynamic-node", "type": "ss",
|
||||
"server": "1.2.3.4", "port": 443}]})
|
||||
cfg = yaml.safe_load(result)
|
||||
names = [p["name"] for p in cfg["proxies"]]
|
||||
assert "static-node" in names
|
||||
assert "dynamic-node" in names
|
||||
|
||||
|
||||
def test_expand_config_empty_providers():
|
||||
result = expand_config(BASE_YAML, {})
|
||||
cfg = yaml.safe_load(result)
|
||||
assert "proxy-providers" not in cfg
|
||||
|
||||
|
||||
def test_build_mihomo_config_merges_providers():
|
||||
yaml1 = """
|
||||
proxy-providers:
|
||||
p1:
|
||||
type: http
|
||||
url: https://a.example.com/sub
|
||||
interval: 3600
|
||||
health-check:
|
||||
enable: true
|
||||
url: http://www.gstatic.com/generate_204
|
||||
interval: 300
|
||||
"""
|
||||
yaml2 = """
|
||||
proxy-providers:
|
||||
p2:
|
||||
type: http
|
||||
url: https://b.example.com/sub
|
||||
interval: 3600
|
||||
"""
|
||||
result = build_mihomo_config([yaml1, yaml2], "mysecret")
|
||||
cfg = yaml.safe_load(result)
|
||||
|
||||
assert cfg["external-controller"] == "0.0.0.0:9090"
|
||||
assert cfg["secret"] == "mysecret"
|
||||
assert cfg["mixed-port"] == 7890
|
||||
assert cfg["allow-lan"] is False
|
||||
assert "p1" in cfg["proxy-providers"]
|
||||
assert "p2" in cfg["proxy-providers"]
|
||||
assert cfg["proxy-providers"]["p1"]["health-check"]["enable"] is True
|
||||
|
||||
|
||||
def test_build_mihomo_config_deduplicates_by_name():
|
||||
yaml1 = """
|
||||
proxy-providers:
|
||||
p1:
|
||||
type: http
|
||||
url: https://original.example.com/sub
|
||||
interval: 3600
|
||||
"""
|
||||
yaml2 = """
|
||||
proxy-providers:
|
||||
p1:
|
||||
type: http
|
||||
url: https://duplicate.example.com/sub
|
||||
interval: 3600
|
||||
"""
|
||||
result = build_mihomo_config([yaml1, yaml2], "s")
|
||||
cfg = yaml.safe_load(result)
|
||||
assert cfg["proxy-providers"]["p1"]["url"] == "https://original.example.com/sub"
|
||||
|
||||
|
||||
def test_build_mihomo_config_no_providers():
|
||||
result = build_mihomo_config(["proxies: []"], "s")
|
||||
cfg = yaml.safe_load(result)
|
||||
assert "proxy-providers" not in cfg
|
||||
Reference in New Issue
Block a user