opt
This commit is contained in:
@@ -55,6 +55,8 @@ def _resolve_sources(sources: str | None) -> list[tuple[str, SourceConfig]]:
|
||||
|
||||
|
||||
def _rule_path(rule: RuleConfig):
|
||||
if not rule.file:
|
||||
raise HTTPException(status_code=404, detail="rule file not available")
|
||||
path = (settings.rules_dir / rule.file).resolve()
|
||||
if not path.is_file() or settings.rules_dir.resolve() not in path.parents:
|
||||
raise HTTPException(status_code=404, detail="rule file missing")
|
||||
|
||||
@@ -19,12 +19,13 @@ class SourceConfig(BaseModel):
|
||||
|
||||
|
||||
class RuleConfig(BaseModel):
|
||||
file: str
|
||||
file: str | None = None
|
||||
behavior: Literal["domain", "ipcidr", "classical"] = "domain"
|
||||
format: Literal["yaml", "text", "mrs"] = "yaml"
|
||||
interval: int = 86400
|
||||
policy: str
|
||||
no_resolve: bool = False
|
||||
payload: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RegionConfig(BaseModel):
|
||||
@@ -33,13 +34,23 @@ class RegionConfig(BaseModel):
|
||||
tolerance: int = 50
|
||||
|
||||
|
||||
class ProxyGroupConfig(BaseModel):
|
||||
name: str
|
||||
type: Literal["select", "url-test"] = "select"
|
||||
proxies: list[str] = Field(default_factory=list)
|
||||
filter: str | None = None
|
||||
tolerance: int | None = None
|
||||
url: HttpUrl | None = None
|
||||
interval: int | None = None
|
||||
|
||||
|
||||
class ClientConfig(BaseModel):
|
||||
title: str
|
||||
provider_interval: int = 21600
|
||||
rule_interval: int = 86400
|
||||
test_url: HttpUrl = "https://www.gstatic.com/generate_204"
|
||||
test_interval: int = 300
|
||||
main_policy: str = "节点选择"
|
||||
main_policy: str = "🚀 节点选择"
|
||||
source_policy: str = "☁️ 机场选择"
|
||||
mixed_auto_policy: str = "♻️ 自动选择"
|
||||
manual_policy: str = "🚀 手动切换"
|
||||
@@ -58,6 +69,8 @@ class AppConfig(BaseModel):
|
||||
rules: dict[str, RuleConfig] = Field(default_factory=dict)
|
||||
clients: dict[str, ClientConfig] = Field(default_factory=dict)
|
||||
regions: dict[str, RegionConfig] = Field(default_factory=dict)
|
||||
selector_groups: list[ProxyGroupConfig] = Field(default_factory=list)
|
||||
policy_groups: list[ProxyGroupConfig] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FetchResult(BaseModel):
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from app.models import AppConfig, ClientConfig, SourceSnapshot
|
||||
from app.models import AppConfig, ClientConfig, ProxyGroupConfig, SourceSnapshot
|
||||
from app.services.rules import build_inline_rules, build_rule_provider_entries, build_rule_set_references
|
||||
|
||||
|
||||
@@ -13,6 +13,107 @@ def dump_yaml(data: dict[str, Any]) -> str:
|
||||
return yaml.safe_dump(data, allow_unicode=True, sort_keys=False, default_flow_style=False)
|
||||
|
||||
|
||||
def _expand_proxy_tokens(
|
||||
proxies: list[str],
|
||||
*,
|
||||
client: ClientConfig,
|
||||
source_auto_names: list[str],
|
||||
selector_names: list[str],
|
||||
) -> list[str]:
|
||||
expanded: list[str] = []
|
||||
tokens = {
|
||||
"{{ main_policy }}": [client.main_policy],
|
||||
"{{main_policy}}": [client.main_policy],
|
||||
"{{ source_policy }}": [client.source_policy],
|
||||
"{{source_policy}}": [client.source_policy],
|
||||
"{{ mixed_auto_policy }}": [client.mixed_auto_policy],
|
||||
"{{mixed_auto_policy}}": [client.mixed_auto_policy],
|
||||
"{{ manual_policy }}": [client.manual_policy],
|
||||
"{{manual_policy}}": [client.manual_policy],
|
||||
"{{ direct_policy }}": [client.direct_policy],
|
||||
"{{direct_policy}}": [client.direct_policy],
|
||||
"{{ source_auto_groups }}": source_auto_names,
|
||||
"{{source_auto_groups}}": source_auto_names,
|
||||
"{{ selector_groups }}": selector_names,
|
||||
"{{selector_groups}}": selector_names,
|
||||
}
|
||||
for item in proxies:
|
||||
expanded.extend(tokens.get(item, [item]))
|
||||
return expanded
|
||||
|
||||
|
||||
def _build_thin_filter_group(
|
||||
*,
|
||||
client_type: str,
|
||||
client: ClientConfig,
|
||||
group: ProxyGroupConfig,
|
||||
selected_source_names: list[str],
|
||||
) -> dict[str, Any]:
|
||||
built: dict[str, Any] = {
|
||||
"name": group.name,
|
||||
"type": group.type,
|
||||
"filter": group.filter,
|
||||
}
|
||||
if group.type == "url-test":
|
||||
built["url"] = str(group.url or client.test_url)
|
||||
built["interval"] = group.interval or client.test_interval
|
||||
if group.tolerance is not None:
|
||||
built["tolerance"] = group.tolerance
|
||||
if client_type == "mihomo":
|
||||
built["use"] = selected_source_names
|
||||
else:
|
||||
built["include-all"] = True
|
||||
return built
|
||||
|
||||
|
||||
def _build_bundle_filter_group(
|
||||
*,
|
||||
client: ClientConfig,
|
||||
group: ProxyGroupConfig,
|
||||
all_proxy_names: list[str],
|
||||
) -> dict[str, Any]:
|
||||
matched = [name for name in all_proxy_names if group.filter and re.search(group.filter, name)]
|
||||
built: dict[str, Any] = {
|
||||
"name": group.name,
|
||||
"type": group.type,
|
||||
"proxies": matched or [client.direct_policy],
|
||||
}
|
||||
if group.type == "url-test":
|
||||
built["url"] = str(group.url or client.test_url)
|
||||
built["interval"] = group.interval or client.test_interval
|
||||
if group.tolerance is not None:
|
||||
built["tolerance"] = group.tolerance
|
||||
return built
|
||||
|
||||
|
||||
def _build_custom_policy_groups(
|
||||
*,
|
||||
app_config: AppConfig,
|
||||
client: ClientConfig,
|
||||
source_auto_names: list[str],
|
||||
selector_names: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
groups: list[dict[str, Any]] = []
|
||||
for group in app_config.policy_groups:
|
||||
built: dict[str, Any] = {
|
||||
"name": group.name,
|
||||
"type": group.type,
|
||||
"proxies": _expand_proxy_tokens(
|
||||
group.proxies,
|
||||
client=client,
|
||||
source_auto_names=source_auto_names,
|
||||
selector_names=selector_names,
|
||||
),
|
||||
}
|
||||
if group.type == "url-test":
|
||||
built["url"] = str(group.url or client.test_url)
|
||||
built["interval"] = group.interval or client.test_interval
|
||||
if group.tolerance is not None:
|
||||
built["tolerance"] = group.tolerance
|
||||
groups.append(built)
|
||||
return groups
|
||||
|
||||
|
||||
def build_thin_profile(
|
||||
*,
|
||||
client_type: str,
|
||||
@@ -156,7 +257,7 @@ def _build_thin_groups(client_type: str, app_config: AppConfig, client: ClientCo
|
||||
|
||||
groups.append(mixed_auto)
|
||||
|
||||
region_names: list[str] = []
|
||||
selector_names: list[str] = []
|
||||
for region in app_config.regions.values():
|
||||
group = {
|
||||
"name": region.name,
|
||||
@@ -171,7 +272,32 @@ def _build_thin_groups(client_type: str, app_config: AppConfig, client: ClientCo
|
||||
else:
|
||||
group["include-all"] = True
|
||||
groups.append(group)
|
||||
region_names.append(region.name)
|
||||
selector_names.append(region.name)
|
||||
|
||||
for selector in app_config.selector_groups:
|
||||
if selector.filter:
|
||||
groups.append(
|
||||
_build_thin_filter_group(
|
||||
client_type=client_type,
|
||||
client=client,
|
||||
group=selector,
|
||||
selected_source_names=selected_source_names,
|
||||
)
|
||||
)
|
||||
else:
|
||||
groups.append(
|
||||
{
|
||||
"name": selector.name,
|
||||
"type": selector.type,
|
||||
"proxies": _expand_proxy_tokens(
|
||||
selector.proxies,
|
||||
client=client,
|
||||
source_auto_names=source_auto_names,
|
||||
selector_names=selector_names,
|
||||
),
|
||||
}
|
||||
)
|
||||
selector_names.append(selector.name)
|
||||
|
||||
groups.append(
|
||||
{
|
||||
@@ -181,6 +307,14 @@ def _build_thin_groups(client_type: str, app_config: AppConfig, client: ClientCo
|
||||
}
|
||||
)
|
||||
groups.append(manual)
|
||||
groups.extend(
|
||||
_build_custom_policy_groups(
|
||||
app_config=app_config,
|
||||
client=client,
|
||||
source_auto_names=source_auto_names,
|
||||
selector_names=selector_names,
|
||||
)
|
||||
)
|
||||
groups.append(
|
||||
{
|
||||
"name": client.main_policy,
|
||||
@@ -188,7 +322,7 @@ def _build_thin_groups(client_type: str, app_config: AppConfig, client: ClientCo
|
||||
"proxies": [
|
||||
client.source_policy,
|
||||
client.mixed_auto_policy,
|
||||
*region_names,
|
||||
*selector_names,
|
||||
client.manual_policy,
|
||||
client.direct_policy,
|
||||
],
|
||||
@@ -230,7 +364,7 @@ def _build_bundle_groups(
|
||||
}
|
||||
)
|
||||
|
||||
region_names: list[str] = []
|
||||
selector_names: list[str] = []
|
||||
for region in app_config.regions.values():
|
||||
matched = [name for name in all_proxy_names if re.search(region.filter, name)]
|
||||
groups.append(
|
||||
@@ -243,7 +377,31 @@ def _build_bundle_groups(
|
||||
"proxies": matched or [client.direct_policy],
|
||||
}
|
||||
)
|
||||
region_names.append(region.name)
|
||||
selector_names.append(region.name)
|
||||
|
||||
for selector in app_config.selector_groups:
|
||||
if selector.filter:
|
||||
groups.append(
|
||||
_build_bundle_filter_group(
|
||||
client=client,
|
||||
group=selector,
|
||||
all_proxy_names=all_proxy_names,
|
||||
)
|
||||
)
|
||||
else:
|
||||
groups.append(
|
||||
{
|
||||
"name": selector.name,
|
||||
"type": selector.type,
|
||||
"proxies": _expand_proxy_tokens(
|
||||
selector.proxies,
|
||||
client=client,
|
||||
source_auto_names=source_auto_names,
|
||||
selector_names=selector_names,
|
||||
),
|
||||
}
|
||||
)
|
||||
selector_names.append(selector.name)
|
||||
|
||||
groups.append(
|
||||
{
|
||||
@@ -259,6 +417,14 @@ def _build_bundle_groups(
|
||||
"proxies": [*all_proxy_names, client.direct_policy] if all_proxy_names else [client.direct_policy],
|
||||
}
|
||||
)
|
||||
groups.extend(
|
||||
_build_custom_policy_groups(
|
||||
app_config=app_config,
|
||||
client=client,
|
||||
source_auto_names=source_auto_names,
|
||||
selector_names=selector_names,
|
||||
)
|
||||
)
|
||||
groups.append(
|
||||
{
|
||||
"name": client.main_policy,
|
||||
@@ -266,7 +432,7 @@ def _build_bundle_groups(
|
||||
"proxies": [
|
||||
client.source_policy,
|
||||
client.mixed_auto_policy,
|
||||
*region_names,
|
||||
*selector_names,
|
||||
client.manual_policy,
|
||||
client.direct_policy,
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import yaml
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models import AppConfig, ClientConfig, RuleConfig
|
||||
from app.models import AppConfig, ClientConfig
|
||||
|
||||
|
||||
def resolve_policy(policy: str, client: ClientConfig) -> str:
|
||||
@@ -38,9 +38,31 @@ def load_rule_payload(path: Path) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def _resolve_rule_lines(rule_name: str, app_config: AppConfig, client: ClientConfig) -> list[str]:
|
||||
rule = app_config.rules[rule_name]
|
||||
target = resolve_policy(rule.policy, client)
|
||||
lines: list[str] = []
|
||||
|
||||
for payload_line in rule.payload:
|
||||
line = f"{payload_line},{target}"
|
||||
if rule.no_resolve:
|
||||
line += ",no-resolve"
|
||||
lines.append(line)
|
||||
|
||||
if rule.file:
|
||||
line = f"RULE-SET,{rule_name},{target}"
|
||||
if rule.no_resolve:
|
||||
line += ",no-resolve"
|
||||
lines.append(line)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def build_rule_provider_entries(app_config: AppConfig, client: ClientConfig, base_url: str, public_path: str):
|
||||
providers: dict[str, dict] = {}
|
||||
for name, rule in app_config.rules.items():
|
||||
if not rule.file:
|
||||
continue
|
||||
entry = {
|
||||
"behavior": rule.behavior,
|
||||
"format": rule.format,
|
||||
@@ -53,12 +75,8 @@ def build_rule_provider_entries(app_config: AppConfig, client: ClientConfig, bas
|
||||
|
||||
def build_rule_set_references(app_config: AppConfig, client: ClientConfig) -> list[str]:
|
||||
refs: list[str] = []
|
||||
for name, rule in app_config.rules.items():
|
||||
target = resolve_policy(rule.policy, client)
|
||||
line = f"RULE-SET,{name},{target}"
|
||||
if rule.no_resolve:
|
||||
line += ",no-resolve"
|
||||
refs.append(line)
|
||||
for name in app_config.rules:
|
||||
refs.extend(_resolve_rule_lines(name, app_config, client))
|
||||
refs.append(f"MATCH,{client.main_policy}")
|
||||
return refs
|
||||
|
||||
@@ -66,7 +84,14 @@ def build_rule_set_references(app_config: AppConfig, client: ClientConfig) -> li
|
||||
def build_inline_rules(app_config: AppConfig, client: ClientConfig) -> list[str]:
|
||||
settings = get_settings()
|
||||
lines: list[str] = []
|
||||
for rule in app_config.rules.values():
|
||||
for name, rule in app_config.rules.items():
|
||||
for payload_line in rule.payload:
|
||||
line = f"{payload_line},{resolve_policy(rule.policy, client)}"
|
||||
if rule.no_resolve:
|
||||
line += ",no-resolve"
|
||||
lines.append(line)
|
||||
if not rule.file:
|
||||
continue
|
||||
path = (settings.rules_dir / rule.file).resolve()
|
||||
if not path.is_file() or settings.rules_dir.resolve() not in path.parents:
|
||||
raise FileNotFoundError(f"Rule file missing: {rule.file}")
|
||||
|
||||
Reference in New Issue
Block a user