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):
|
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()
|
path = (settings.rules_dir / rule.file).resolve()
|
||||||
if not path.is_file() or settings.rules_dir.resolve() not in path.parents:
|
if not path.is_file() or settings.rules_dir.resolve() not in path.parents:
|
||||||
raise HTTPException(status_code=404, detail="rule file missing")
|
raise HTTPException(status_code=404, detail="rule file missing")
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ class SourceConfig(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class RuleConfig(BaseModel):
|
class RuleConfig(BaseModel):
|
||||||
file: str
|
file: str | None = None
|
||||||
behavior: Literal["domain", "ipcidr", "classical"] = "domain"
|
behavior: Literal["domain", "ipcidr", "classical"] = "domain"
|
||||||
format: Literal["yaml", "text", "mrs"] = "yaml"
|
format: Literal["yaml", "text", "mrs"] = "yaml"
|
||||||
interval: int = 86400
|
interval: int = 86400
|
||||||
policy: str
|
policy: str
|
||||||
no_resolve: bool = False
|
no_resolve: bool = False
|
||||||
|
payload: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class RegionConfig(BaseModel):
|
class RegionConfig(BaseModel):
|
||||||
@@ -33,13 +34,23 @@ class RegionConfig(BaseModel):
|
|||||||
tolerance: int = 50
|
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):
|
class ClientConfig(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
provider_interval: int = 21600
|
provider_interval: int = 21600
|
||||||
rule_interval: int = 86400
|
rule_interval: int = 86400
|
||||||
test_url: HttpUrl = "https://www.gstatic.com/generate_204"
|
test_url: HttpUrl = "https://www.gstatic.com/generate_204"
|
||||||
test_interval: int = 300
|
test_interval: int = 300
|
||||||
main_policy: str = "节点选择"
|
main_policy: str = "🚀 节点选择"
|
||||||
source_policy: str = "☁️ 机场选择"
|
source_policy: str = "☁️ 机场选择"
|
||||||
mixed_auto_policy: str = "♻️ 自动选择"
|
mixed_auto_policy: str = "♻️ 自动选择"
|
||||||
manual_policy: str = "🚀 手动切换"
|
manual_policy: str = "🚀 手动切换"
|
||||||
@@ -58,6 +69,8 @@ class AppConfig(BaseModel):
|
|||||||
rules: dict[str, RuleConfig] = Field(default_factory=dict)
|
rules: dict[str, RuleConfig] = Field(default_factory=dict)
|
||||||
clients: dict[str, ClientConfig] = Field(default_factory=dict)
|
clients: dict[str, ClientConfig] = Field(default_factory=dict)
|
||||||
regions: dict[str, RegionConfig] = 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):
|
class FetchResult(BaseModel):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any
|
|||||||
|
|
||||||
import yaml
|
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
|
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)
|
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(
|
def build_thin_profile(
|
||||||
*,
|
*,
|
||||||
client_type: str,
|
client_type: str,
|
||||||
@@ -156,7 +257,7 @@ def _build_thin_groups(client_type: str, app_config: AppConfig, client: ClientCo
|
|||||||
|
|
||||||
groups.append(mixed_auto)
|
groups.append(mixed_auto)
|
||||||
|
|
||||||
region_names: list[str] = []
|
selector_names: list[str] = []
|
||||||
for region in app_config.regions.values():
|
for region in app_config.regions.values():
|
||||||
group = {
|
group = {
|
||||||
"name": region.name,
|
"name": region.name,
|
||||||
@@ -171,7 +272,32 @@ def _build_thin_groups(client_type: str, app_config: AppConfig, client: ClientCo
|
|||||||
else:
|
else:
|
||||||
group["include-all"] = True
|
group["include-all"] = True
|
||||||
groups.append(group)
|
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(
|
groups.append(
|
||||||
{
|
{
|
||||||
@@ -181,6 +307,14 @@ def _build_thin_groups(client_type: str, app_config: AppConfig, client: ClientCo
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
groups.append(manual)
|
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(
|
groups.append(
|
||||||
{
|
{
|
||||||
"name": client.main_policy,
|
"name": client.main_policy,
|
||||||
@@ -188,7 +322,7 @@ def _build_thin_groups(client_type: str, app_config: AppConfig, client: ClientCo
|
|||||||
"proxies": [
|
"proxies": [
|
||||||
client.source_policy,
|
client.source_policy,
|
||||||
client.mixed_auto_policy,
|
client.mixed_auto_policy,
|
||||||
*region_names,
|
*selector_names,
|
||||||
client.manual_policy,
|
client.manual_policy,
|
||||||
client.direct_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():
|
for region in app_config.regions.values():
|
||||||
matched = [name for name in all_proxy_names if re.search(region.filter, name)]
|
matched = [name for name in all_proxy_names if re.search(region.filter, name)]
|
||||||
groups.append(
|
groups.append(
|
||||||
@@ -243,7 +377,31 @@ def _build_bundle_groups(
|
|||||||
"proxies": matched or [client.direct_policy],
|
"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(
|
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],
|
"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(
|
groups.append(
|
||||||
{
|
{
|
||||||
"name": client.main_policy,
|
"name": client.main_policy,
|
||||||
@@ -266,7 +432,7 @@ def _build_bundle_groups(
|
|||||||
"proxies": [
|
"proxies": [
|
||||||
client.source_policy,
|
client.source_policy,
|
||||||
client.mixed_auto_policy,
|
client.mixed_auto_policy,
|
||||||
*region_names,
|
*selector_names,
|
||||||
client.manual_policy,
|
client.manual_policy,
|
||||||
client.direct_policy,
|
client.direct_policy,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from app.config import get_settings
|
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:
|
def resolve_policy(policy: str, client: ClientConfig) -> str:
|
||||||
@@ -38,9 +38,31 @@ def load_rule_payload(path: Path) -> list[str]:
|
|||||||
return lines
|
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):
|
def build_rule_provider_entries(app_config: AppConfig, client: ClientConfig, base_url: str, public_path: str):
|
||||||
providers: dict[str, dict] = {}
|
providers: dict[str, dict] = {}
|
||||||
for name, rule in app_config.rules.items():
|
for name, rule in app_config.rules.items():
|
||||||
|
if not rule.file:
|
||||||
|
continue
|
||||||
entry = {
|
entry = {
|
||||||
"behavior": rule.behavior,
|
"behavior": rule.behavior,
|
||||||
"format": rule.format,
|
"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]:
|
def build_rule_set_references(app_config: AppConfig, client: ClientConfig) -> list[str]:
|
||||||
refs: list[str] = []
|
refs: list[str] = []
|
||||||
for name, rule in app_config.rules.items():
|
for name in app_config.rules:
|
||||||
target = resolve_policy(rule.policy, client)
|
refs.extend(_resolve_rule_lines(name, app_config, client))
|
||||||
line = f"RULE-SET,{name},{target}"
|
|
||||||
if rule.no_resolve:
|
|
||||||
line += ",no-resolve"
|
|
||||||
refs.append(line)
|
|
||||||
refs.append(f"MATCH,{client.main_policy}")
|
refs.append(f"MATCH,{client.main_policy}")
|
||||||
return refs
|
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]:
|
def build_inline_rules(app_config: AppConfig, client: ClientConfig) -> list[str]:
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
lines: list[str] = []
|
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()
|
path = (settings.rules_dir / rule.file).resolve()
|
||||||
if not path.is_file() or settings.rules_dir.resolve() not in path.parents:
|
if not path.is_file() or settings.rules_dir.resolve() not in path.parents:
|
||||||
raise FileNotFoundError(f"Rule file missing: {rule.file}")
|
raise FileNotFoundError(f"Rule file missing: {rule.file}")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
payload:
|
payload:
|
||||||
- DOMAIN-SUFFIX,lan
|
- DOMAIN-SUFFIX,lan
|
||||||
- DOMAIN-SUFFIX,local
|
- DOMAIN-SUFFIX,local
|
||||||
- DOMAIN-SUFFIX,apple.com
|
- DOMAIN-SUFFIX,localdomain
|
||||||
- DOMAIN-SUFFIX,icloud.com
|
- DOMAIN-SUFFIX,home.arpa
|
||||||
|
- DOMAIN-SUFFIX,msftconnecttest.com
|
||||||
|
- DOMAIN-SUFFIX,msftncsi.com
|
||||||
|
|||||||
@@ -21,23 +21,233 @@ sources:
|
|||||||
|
|
||||||
regions:
|
regions:
|
||||||
hk:
|
hk:
|
||||||
name: "🇭🇰 香港自动"
|
name: "🇭🇰 香港节点"
|
||||||
filter: "(?i)(香港|hk|hong kong)"
|
filter: "(?i)(港|hk|hong kong|hongkong)"
|
||||||
|
tolerance: 50
|
||||||
|
tw:
|
||||||
|
name: "🇨🇳 台湾节点"
|
||||||
|
filter: "(?i)(台|新北|彰化|tw|taiwan)"
|
||||||
tolerance: 50
|
tolerance: 50
|
||||||
sg:
|
sg:
|
||||||
name: "🇸🇬 新加坡自动"
|
name: "🇸🇬 狮城节点"
|
||||||
filter: "(?i)(新加坡|狮城|sg|singapore)"
|
filter: "(?i)(新加坡|坡|狮城|sg|singapore)"
|
||||||
tolerance: 50
|
tolerance: 50
|
||||||
jp:
|
jp:
|
||||||
name: "🇯🇵 日本自动"
|
name: "🇯🇵 日本节点"
|
||||||
filter: "(?i)(日本|jp|japan)"
|
filter: "(?i)(日本|东京|大阪|埼玉|jp|japan)"
|
||||||
tolerance: 50
|
tolerance: 50
|
||||||
us:
|
us:
|
||||||
name: "🇺🇸 美国自动"
|
name: "🇺🇲 美国节点"
|
||||||
filter: "(?i)(美国|美國|us|united states)"
|
filter: "(?i)(美|波特兰|达拉斯|俄勒冈|凤凰城|费利蒙|硅谷|拉斯维加斯|洛杉矶|圣何塞|圣克拉拉|西雅图|芝加哥|us|united states)"
|
||||||
tolerance: 150
|
tolerance: 150
|
||||||
|
kr:
|
||||||
|
name: "🇰🇷 韩国节点"
|
||||||
|
filter: "(?i)(kr|korea|kor|首尔|韩|韓)"
|
||||||
|
tolerance: 50
|
||||||
|
|
||||||
|
selector_groups:
|
||||||
|
- name: "🎥 奈飞节点"
|
||||||
|
type: select
|
||||||
|
filter: "(?i)(nf|奈飞|解锁|netflix|media)"
|
||||||
|
|
||||||
|
policy_groups:
|
||||||
|
- name: "📲 电报消息"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ mixed_auto_policy }}"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- name: "💬 Ai平台"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ mixed_auto_policy }}"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- name: "📹 油管视频"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ mixed_auto_policy }}"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- name: "🎥 奈飞视频"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "🎥 奈飞节点"
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ mixed_auto_policy }}"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- name: "🌍 国外媒体"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ mixed_auto_policy }}"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- name: "📢 谷歌"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- name: "Ⓜ️ 微软Bing"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- name: "Ⓜ️ 微软云盘"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- name: "Ⓜ️ 微软服务"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- name: "🍎 苹果服务"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- name: "🎮 游戏平台"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- name: "🎮 PT平台"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "🇭🇰 香港节点"
|
||||||
|
- "🇨🇳 台湾节点"
|
||||||
|
- "🇸🇬 狮城节点"
|
||||||
|
- "🇯🇵 日本节点"
|
||||||
|
- "🇺🇲 美国节点"
|
||||||
|
- "🇰🇷 韩国节点"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
- name: "🎯 全球直连"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ mixed_auto_policy }}"
|
||||||
|
- name: "🛑 广告拦截"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- REJECT
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- name: "🍃 应用净化"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- REJECT
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- name: "🐟 漏网之鱼"
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- "{{ direct_policy }}"
|
||||||
|
- "{{ main_policy }}"
|
||||||
|
- "{{ mixed_auto_policy }}"
|
||||||
|
- "{{ selector_groups }}"
|
||||||
|
- "{{ manual_policy }}"
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
|
custom-proxy:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "{{ main_policy }}"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,cloudflare
|
||||||
|
- DOMAIN-KEYWORD,hetzner
|
||||||
|
- DOMAIN-KEYWORD,github
|
||||||
|
- DOMAIN-KEYWORD,google
|
||||||
|
- DOMAIN-KEYWORD,vscode
|
||||||
|
- DOMAIN-KEYWORD,telegram
|
||||||
|
- DOMAIN-KEYWORD,youtube
|
||||||
|
- DOMAIN-KEYWORD,whatsapp
|
||||||
|
- DOMAIN-KEYWORD,dropbox
|
||||||
|
- DOMAIN-KEYWORD,facebook
|
||||||
|
- DOMAIN-KEYWORD,twitter
|
||||||
|
- DOMAIN-KEYWORD,instagram
|
||||||
|
- DOMAIN-KEYWORD,spotify
|
||||||
|
- DOMAIN-KEYWORD,sci-hub
|
||||||
|
- DOMAIN-KEYWORD,1e100
|
||||||
|
|
||||||
reject:
|
reject:
|
||||||
file: reject.yaml
|
file: reject.yaml
|
||||||
behavior: domain
|
behavior: domain
|
||||||
@@ -45,26 +255,193 @@ rules:
|
|||||||
interval: 86400
|
interval: 86400
|
||||||
policy: REJECT
|
policy: REJECT
|
||||||
|
|
||||||
|
app-purify:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "🍃 应用净化"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,omgmtaw
|
||||||
|
|
||||||
direct:
|
direct:
|
||||||
file: direct.yaml
|
file: direct.yaml
|
||||||
behavior: domain
|
behavior: domain
|
||||||
format: yaml
|
format: yaml
|
||||||
interval: 86400
|
interval: 86400
|
||||||
policy: "{{ direct_policy }}"
|
policy: "🎯 全球直连"
|
||||||
|
|
||||||
proxy:
|
local-network:
|
||||||
file: proxy.yaml
|
behavior: classical
|
||||||
behavior: domain
|
format: text
|
||||||
format: yaml
|
policy: "🎯 全球直连"
|
||||||
interval: 86400
|
no_resolve: true
|
||||||
policy: "{{ main_policy }}"
|
payload:
|
||||||
|
- IP-CIDR,10.0.0.0/8
|
||||||
|
- IP-CIDR,172.16.0.0/12
|
||||||
|
- IP-CIDR,192.168.0.0/16
|
||||||
|
|
||||||
|
google:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "📢 谷歌"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,gmail
|
||||||
|
- DOMAIN-KEYWORD,google
|
||||||
|
- DOMAIN-SUFFIX,googleapis.com
|
||||||
|
- DOMAIN-SUFFIX,gstatic.com
|
||||||
|
- DOMAIN-SUFFIX,googlevideo.com
|
||||||
|
|
||||||
|
microsoft-bing:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "Ⓜ️ 微软Bing"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,bing
|
||||||
|
|
||||||
|
microsoft-onedrive:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "Ⓜ️ 微软云盘"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,1drv
|
||||||
|
- DOMAIN-KEYWORD,onedrive
|
||||||
|
- DOMAIN-KEYWORD,skydrive
|
||||||
|
|
||||||
|
microsoft:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "Ⓜ️ 微软服务"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,microsoft
|
||||||
|
- DOMAIN-SUFFIX,live.com
|
||||||
|
- DOMAIN-SUFFIX,windows.com
|
||||||
|
- DOMAIN-SUFFIX,microsoftonline.com
|
||||||
|
|
||||||
|
apple:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "🍎 苹果服务"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,apple
|
||||||
|
- DOMAIN-SUFFIX,icloud.com
|
||||||
|
- DOMAIN-SUFFIX,apple.com
|
||||||
|
|
||||||
|
telegram:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "📲 电报消息"
|
||||||
|
no_resolve: true
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,telegram
|
||||||
|
- DOMAIN-SUFFIX,t.me
|
||||||
|
- DOMAIN-SUFFIX,telegram.me
|
||||||
|
- DOMAIN-SUFFIX,telegram.org
|
||||||
|
- IP-CIDR,91.108.4.0/22
|
||||||
|
- IP-CIDR,91.108.8.0/21
|
||||||
|
- IP-CIDR,91.108.16.0/22
|
||||||
|
- IP-CIDR,91.108.56.0/22
|
||||||
|
- IP-CIDR,149.154.160.0/20
|
||||||
|
|
||||||
|
ai:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "💬 Ai平台"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,openai
|
||||||
|
- DOMAIN-KEYWORD,anthropic
|
||||||
|
- DOMAIN-KEYWORD,claude
|
||||||
|
- DOMAIN-SUFFIX,chatgpt.com
|
||||||
|
- DOMAIN-SUFFIX,openai.com
|
||||||
|
- DOMAIN-SUFFIX,oaistatic.com
|
||||||
|
- DOMAIN-SUFFIX,oaiusercontent.com
|
||||||
|
- DOMAIN-SUFFIX,anthropic.com
|
||||||
|
- DOMAIN-SUFFIX,claude.ai
|
||||||
|
|
||||||
|
youtube:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "📹 油管视频"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,youtube
|
||||||
|
- DOMAIN-SUFFIX,youtu.be
|
||||||
|
- DOMAIN-SUFFIX,youtube.com
|
||||||
|
- DOMAIN-SUFFIX,ytimg.com
|
||||||
|
- DOMAIN-SUFFIX,googlevideo.com
|
||||||
|
|
||||||
|
netflix:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "🎥 奈飞视频"
|
||||||
|
no_resolve: true
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,netflix
|
||||||
|
- DOMAIN-KEYWORD,netflixdnstest
|
||||||
|
- DOMAIN-KEYWORD,apiproxy-device-prod-nlb-
|
||||||
|
- DOMAIN-KEYWORD,dualstack.apiproxy-
|
||||||
|
- DOMAIN-SUFFIX,netflix.com
|
||||||
|
- DOMAIN-SUFFIX,nflxvideo.net
|
||||||
|
- DOMAIN-SUFFIX,nflximg.net
|
||||||
|
- IP-CIDR,23.246.0.0/18
|
||||||
|
- IP-CIDR,37.77.184.0/21
|
||||||
|
|
||||||
|
proxy-media:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "🌍 国外媒体"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,spotify
|
||||||
|
- DOMAIN-KEYWORD,tiktokcdn
|
||||||
|
- DOMAIN-KEYWORD,ttvnw
|
||||||
|
- DOMAIN-KEYWORD,jooxweb-api
|
||||||
|
- DOMAIN-KEYWORD,hbogoasia
|
||||||
|
- DOMAIN-KEYWORD,nowtv100
|
||||||
|
- DOMAIN-KEYWORD,rthklive
|
||||||
|
- DOMAIN-KEYWORD,bbc
|
||||||
|
- DOMAIN-KEYWORD,youtube
|
||||||
|
- DOMAIN-SUFFIX,open.spotify.com
|
||||||
|
|
||||||
|
games:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "🎮 游戏平台"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,epicgames
|
||||||
|
- DOMAIN-KEYWORD,origin
|
||||||
|
- DOMAIN-KEYWORD,steam
|
||||||
|
- DOMAIN-KEYWORD,nintendo
|
||||||
|
- DOMAIN-SUFFIX,steampowered.com
|
||||||
|
- DOMAIN-SUFFIX,steamcontent.com
|
||||||
|
- DOMAIN-SUFFIX,steamusercontent.com
|
||||||
|
|
||||||
|
pt:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "🎮 PT平台"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,btschool
|
||||||
|
- DOMAIN-KEYWORD,hdkylin
|
||||||
|
- DOMAIN-KEYWORD,cloudflare
|
||||||
|
- DOMAIN-KEYWORD,hetzner
|
||||||
|
|
||||||
|
cn-domain:
|
||||||
|
behavior: classical
|
||||||
|
format: text
|
||||||
|
policy: "🎯 全球直连"
|
||||||
|
payload:
|
||||||
|
- DOMAIN-KEYWORD,360buy
|
||||||
|
- DOMAIN-KEYWORD,alicdn
|
||||||
|
- DOMAIN-KEYWORD,alipay
|
||||||
|
- DOMAIN-KEYWORD,baidu
|
||||||
|
- DOMAIN-KEYWORD,bilibili
|
||||||
|
- DOMAIN-KEYWORD,jd
|
||||||
|
- DOMAIN-KEYWORD,qhimg
|
||||||
|
- DOMAIN-KEYWORD,xiaomi
|
||||||
|
|
||||||
cn-ip:
|
cn-ip:
|
||||||
file: cn-ip.yaml
|
file: cn-ip.yaml
|
||||||
behavior: ipcidr
|
behavior: ipcidr
|
||||||
format: yaml
|
format: yaml
|
||||||
interval: 86400
|
interval: 86400
|
||||||
policy: "{{ direct_policy }}"
|
policy: "🎯 全球直连"
|
||||||
no_resolve: true
|
no_resolve: true
|
||||||
|
|
||||||
clients:
|
clients:
|
||||||
@@ -74,7 +451,7 @@ clients:
|
|||||||
rule_interval: 86400
|
rule_interval: 86400
|
||||||
test_url: https://www.gstatic.com/generate_204
|
test_url: https://www.gstatic.com/generate_204
|
||||||
test_interval: 300
|
test_interval: 300
|
||||||
main_policy: 节点选择
|
main_policy: 🚀 节点选择
|
||||||
source_policy: ☁️ 机场选择
|
source_policy: ☁️ 机场选择
|
||||||
mixed_auto_policy: ♻️ 自动选择
|
mixed_auto_policy: ♻️ 自动选择
|
||||||
manual_policy: 🚀 手动切换
|
manual_policy: 🚀 手动切换
|
||||||
@@ -92,7 +469,7 @@ clients:
|
|||||||
rule_interval: 86400
|
rule_interval: 86400
|
||||||
test_url: https://www.gstatic.com/generate_204
|
test_url: https://www.gstatic.com/generate_204
|
||||||
test_interval: 300
|
test_interval: 300
|
||||||
main_policy: 节点选择
|
main_policy: 🚀 节点选择
|
||||||
source_policy: ☁️ 机场选择
|
source_policy: ☁️ 机场选择
|
||||||
mixed_auto_policy: ♻️ 自动选择
|
mixed_auto_policy: ♻️ 自动选择
|
||||||
manual_policy: 🚀 手动切换
|
manual_policy: 🚀 手动切换
|
||||||
|
|||||||
652
sub-provider-project-context.md
Normal file
652
sub-provider-project-context.md
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
# sub-provider 项目上下文说明
|
||||||
|
|
||||||
|
本文档用于给后续接手的 Codex / 开发者快速建立上下文,理解这个项目为什么存在、当前已经做了什么、设计取舍是什么、下一步应该往哪里继续。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目背景
|
||||||
|
|
||||||
|
用户当前的代理使用场景比较复杂:
|
||||||
|
|
||||||
|
- Windows 主力客户端:**Mihomo Party / Clash Party**
|
||||||
|
- iOS 客户端:**Stash**
|
||||||
|
- 路由侧:**OpenClash**
|
||||||
|
- 上游机场源不止一个,其中至少有一个已经变成了 **AnyTLS**
|
||||||
|
- 过去主要依赖 `subconverter` / `subconverter-extended` / ACLSSR 模板体系做转换
|
||||||
|
|
||||||
|
用户遇到的问题是:
|
||||||
|
|
||||||
|
1. 上游协议变复杂后,传统模板式转换越来越脆弱。
|
||||||
|
2. 想统一管理多个机场源,但不想把所有逻辑继续绑死在单个大 YAML 里。
|
||||||
|
3. 希望同时兼容两种使用习惯:
|
||||||
|
- 客户端直接拉一个完整配置文件
|
||||||
|
- 客户端拉一个轻量壳子,然后继续通过 provider 自动更新节点和规则
|
||||||
|
4. 希望保留订阅使用量 / 到期时间展示能力,即把上游订阅返回的 `Subscription-Userinfo` 头继续传给客户端。
|
||||||
|
|
||||||
|
因此,这个项目不是单纯的“订阅转换器”,而是一个 **订阅聚合与配置组装后端**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 本项目的核心目标
|
||||||
|
|
||||||
|
这个项目的目标分成两层:
|
||||||
|
|
||||||
|
### 2.1 内部目标
|
||||||
|
|
||||||
|
服务端内部继续**解耦维护**,不要回退成“一坨最终 YAML”。
|
||||||
|
|
||||||
|
内部要拆开维护:
|
||||||
|
|
||||||
|
- 机场源定义
|
||||||
|
- 节点拉取与标准化
|
||||||
|
- provider 输出
|
||||||
|
- 规则文件
|
||||||
|
- 客户端入口配置
|
||||||
|
- bundle 单文件配置
|
||||||
|
- 配额头处理
|
||||||
|
|
||||||
|
### 2.2 外部目标
|
||||||
|
|
||||||
|
对客户端同时提供两种模式:
|
||||||
|
|
||||||
|
#### A. 薄壳模式
|
||||||
|
客户端拉:
|
||||||
|
|
||||||
|
- `/clients/mihomo.yaml`
|
||||||
|
- `/clients/stash.yaml`
|
||||||
|
|
||||||
|
特点:
|
||||||
|
|
||||||
|
- 配置很薄
|
||||||
|
- 内部继续引用远程 `proxy-providers` / `rule-providers`
|
||||||
|
- 节点和规则按 `interval` 自动更新
|
||||||
|
- 更适合 Mihomo / OpenClash 的长期维护
|
||||||
|
|
||||||
|
#### B. Bundle 模式
|
||||||
|
客户端拉:
|
||||||
|
|
||||||
|
- `/bundle/mihomo.yaml`
|
||||||
|
- `/bundle/stash.yaml`
|
||||||
|
|
||||||
|
特点:
|
||||||
|
|
||||||
|
- 服务端把 `proxies`、`proxy-groups`、`rules` 全部展开成一个完整 YAML
|
||||||
|
- 更适合想“一条链接直接导入”的场景
|
||||||
|
- 响应头继续带上订阅配额信息,方便 Stash / Clash Party 展示流量和到期
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 已确认的产品决策
|
||||||
|
|
||||||
|
下面这些不是临时实现细节,而是本项目当前的明确产品约定。
|
||||||
|
|
||||||
|
### 3.1 支持多机场源选择
|
||||||
|
所有主要接口都支持:
|
||||||
|
|
||||||
|
```text
|
||||||
|
?sources=airport-a,airport-b
|
||||||
|
```
|
||||||
|
|
||||||
|
行为:
|
||||||
|
|
||||||
|
- 不传 `sources`:默认使用所有启用的源
|
||||||
|
- 传 `sources`:只使用指定源,按参数顺序处理
|
||||||
|
- 自动去重
|
||||||
|
- 禁用或不存在的源返回 404
|
||||||
|
|
||||||
|
### 3.2 配额头策略
|
||||||
|
|
||||||
|
用户明确要求:
|
||||||
|
|
||||||
|
- 只选一个机场源时:返回这个机场源的订阅配额信息
|
||||||
|
- 选多个机场源时:**只取第一个源**的配额信息
|
||||||
|
|
||||||
|
因此当前项目统一采用:
|
||||||
|
|
||||||
|
- `Subscription-Userinfo` 只看 `sources` 参数中的第一个源
|
||||||
|
- 不对多个机场的总量做相加、估算或“智能合并”
|
||||||
|
|
||||||
|
这么做的原因是避免语义混乱。不同机场的总量、周期、重置时间可能完全不同,强行合成会误导客户端。
|
||||||
|
|
||||||
|
### 3.3 GET / HEAD 都要支持
|
||||||
|
|
||||||
|
所有核心 YAML 接口同时支持:
|
||||||
|
|
||||||
|
- `GET`
|
||||||
|
- `HEAD`
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 某些客户端会用 `HEAD` 先探测订阅信息或减少流量消耗
|
||||||
|
- 即使不返回 body,也要保留正确的响应头
|
||||||
|
|
||||||
|
当前实现策略:
|
||||||
|
|
||||||
|
- `GET` 返回完整内容 + 响应头
|
||||||
|
- `HEAD` 返回空 body + 相同响应头
|
||||||
|
|
||||||
|
### 3.4 当前版本只支持上游已输出 Clash/Mihomo YAML `proxies:`
|
||||||
|
|
||||||
|
这是一个有意为之的阶段性约束。
|
||||||
|
|
||||||
|
当前项目假定上游 URL 返回的是已经能被解析成:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
proxies:
|
||||||
|
- name: ...
|
||||||
|
type: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
的 YAML 文档。
|
||||||
|
|
||||||
|
这样可以先接用户现有的:
|
||||||
|
|
||||||
|
- sub-wrapper
|
||||||
|
- sub-store
|
||||||
|
- subconverter
|
||||||
|
- subconverter-extended
|
||||||
|
|
||||||
|
等输出,先把整体架构跑通。
|
||||||
|
|
||||||
|
**当前还没有做:**
|
||||||
|
|
||||||
|
- 原始 URI 订阅解析
|
||||||
|
- base64 Clash 订阅解析
|
||||||
|
- sing-box / Surge / Loon 原生格式输入解析
|
||||||
|
|
||||||
|
这是后续扩展点,不是当前版本目标。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 本次主要做了哪些工作
|
||||||
|
|
||||||
|
### 4.1 重新定义项目结构
|
||||||
|
|
||||||
|
把项目拆成了这些核心层:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sub-provider/
|
||||||
|
app/
|
||||||
|
config.py
|
||||||
|
main.py
|
||||||
|
models.py
|
||||||
|
services/
|
||||||
|
cache.py
|
||||||
|
headers.py
|
||||||
|
loader.py
|
||||||
|
profiles.py
|
||||||
|
rules.py
|
||||||
|
subscriptions.py
|
||||||
|
config/
|
||||||
|
sources.yaml
|
||||||
|
rules/
|
||||||
|
reject.yaml
|
||||||
|
direct.yaml
|
||||||
|
proxy.yaml
|
||||||
|
cn-ip.yaml
|
||||||
|
data/
|
||||||
|
cache/
|
||||||
|
.env.example
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yaml
|
||||||
|
requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 避免所有逻辑都塞进 `main.py`
|
||||||
|
- 让后续 Codex 能单独替换抓取层、规则层、模板层、缓存层
|
||||||
|
- 为以后增加 `regions.yaml` / `policies.yaml` / 解析器模块做准备
|
||||||
|
|
||||||
|
### 4.2 增加了配置装载与模型层
|
||||||
|
|
||||||
|
抽离了:
|
||||||
|
|
||||||
|
- App 级配置
|
||||||
|
- SourceConfig
|
||||||
|
- ClientConfig
|
||||||
|
- RuleConfig
|
||||||
|
- RegionConfig
|
||||||
|
- 配额结构
|
||||||
|
- Provider 文档结构
|
||||||
|
- SourceSnapshot
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 让服务内部的处理单位明确
|
||||||
|
- 避免后续继续传裸 dict
|
||||||
|
- 方便 Codex 在 builder / parser / renderer 之间改造
|
||||||
|
|
||||||
|
### 4.3 增加了 provider 层输出能力
|
||||||
|
|
||||||
|
新增能力:
|
||||||
|
|
||||||
|
- 单源 provider 输出:`/providers/{name}.yaml`
|
||||||
|
- 多源 merged provider 输出:`/providers/merged.yaml?sources=...`
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 支持薄壳模式的 `proxy-providers`
|
||||||
|
- 支持调试和人工检查节点池
|
||||||
|
- 为后续做“按地区 provider / 按能力 provider”打基础
|
||||||
|
|
||||||
|
### 4.4 增加了 thin client 输出能力
|
||||||
|
|
||||||
|
新增接口:
|
||||||
|
|
||||||
|
- `/clients/mihomo.yaml`
|
||||||
|
- `/clients/stash.yaml`
|
||||||
|
|
||||||
|
thin 模式特点:
|
||||||
|
|
||||||
|
- 不内联全部节点
|
||||||
|
- 继续引用 `/providers/{name}.yaml`
|
||||||
|
- 继续引用 `/rules/{name}.yaml`
|
||||||
|
- 只生成基础策略组
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 让 Mihomo / OpenClash 保持 provider 驱动的更新机制
|
||||||
|
- 避免每次节点变化都要整份配置重拉
|
||||||
|
|
||||||
|
### 4.5 增加了 bundle 单文件输出能力
|
||||||
|
|
||||||
|
新增接口:
|
||||||
|
|
||||||
|
- `/bundle/mihomo.yaml`
|
||||||
|
- `/bundle/stash.yaml`
|
||||||
|
|
||||||
|
bundle 模式特点:
|
||||||
|
|
||||||
|
- 服务端把所有源节点合并后写进 `proxies`
|
||||||
|
- 服务端生成最终 `proxy-groups`
|
||||||
|
- 服务端把规则集直接内联到 `rules`
|
||||||
|
- 客户端只需要一条最终配置 URL
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 满足用户“客户端直接拉一个完整 YAML”的需求
|
||||||
|
- 保留订阅头展示能力
|
||||||
|
- 方便 Clash Party / Stash 这类更偏向“配置订阅”的使用方式
|
||||||
|
|
||||||
|
### 4.6 增加了 Subscription-Userinfo 头处理
|
||||||
|
|
||||||
|
实现了:
|
||||||
|
|
||||||
|
- 从上游响应头读取配额信息
|
||||||
|
- 解析出 upload / download / total / expire
|
||||||
|
- 在输出接口上重新写回 `Subscription-Userinfo`
|
||||||
|
- 对多源情况只取第一个源
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 让客户端继续显示订阅用量、到期时间
|
||||||
|
- 不要求客户端必须直接访问机场原始订阅
|
||||||
|
|
||||||
|
### 4.7 补全 HEAD 支持
|
||||||
|
|
||||||
|
在以下接口上都支持 `HEAD`:
|
||||||
|
|
||||||
|
- `/providers/{name}.yaml`
|
||||||
|
- `/providers/merged.yaml`
|
||||||
|
- `/rules/{name}.yaml`
|
||||||
|
- `/clients/{client}.yaml`
|
||||||
|
- `/bundle/{client}.yaml`
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 兼容会先做探测的客户端
|
||||||
|
- 减少不必要的 body 传输
|
||||||
|
|
||||||
|
### 4.8 默认生成了一套基础策略组
|
||||||
|
|
||||||
|
当前基础策略结构包括:
|
||||||
|
|
||||||
|
- `☁️ 机场选择`
|
||||||
|
- `♻️ 自动选择`
|
||||||
|
- `🚀 手动切换`
|
||||||
|
- `🇭🇰 香港自动`
|
||||||
|
- `🇸🇬 新加坡自动`
|
||||||
|
- `🇯🇵 日本自动`
|
||||||
|
- `🇺🇸 美国自动`
|
||||||
|
- `节点选择`
|
||||||
|
|
||||||
|
当前逻辑:
|
||||||
|
|
||||||
|
- 每个源会生成一个“{display_name} 自动”组
|
||||||
|
- 一个混合自动组聚合所有源
|
||||||
|
- 地区组根据节点名正则匹配
|
||||||
|
- 主策略组最终统一指向 `节点选择`
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 先提供最小可用的选择层
|
||||||
|
- 给后续做“provider 选择 → 地区节点选择 → 业务规则选择”留位置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 当前接口约定
|
||||||
|
|
||||||
|
### 5.1 健康检查
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 单 provider
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /<PUBLIC_PATH>/providers/{name}.yaml
|
||||||
|
HEAD /<PUBLIC_PATH>/providers/{name}.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
用途:返回指定机场源的 provider 文件。
|
||||||
|
|
||||||
|
### 5.3 merged provider
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /<PUBLIC_PATH>/providers/merged.yaml?sources=airport-a,airport-b
|
||||||
|
HEAD /<PUBLIC_PATH>/providers/merged.yaml?sources=airport-a,airport-b
|
||||||
|
```
|
||||||
|
|
||||||
|
用途:把多个源合成一个 provider 文件。
|
||||||
|
|
||||||
|
### 5.4 rule-provider
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /<PUBLIC_PATH>/rules/{name}.yaml
|
||||||
|
HEAD /<PUBLIC_PATH>/rules/{name}.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
用途:返回单独规则文件。
|
||||||
|
|
||||||
|
### 5.5 thin client
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /<PUBLIC_PATH>/clients/mihomo.yaml?sources=...
|
||||||
|
HEAD /<PUBLIC_PATH>/clients/mihomo.yaml?sources=...
|
||||||
|
GET /<PUBLIC_PATH>/clients/stash.yaml?sources=...
|
||||||
|
HEAD /<PUBLIC_PATH>/clients/stash.yaml?sources=...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 bundle client
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /<PUBLIC_PATH>/bundle/mihomo.yaml?sources=...
|
||||||
|
HEAD /<PUBLIC_PATH>/bundle/mihomo.yaml?sources=...
|
||||||
|
GET /<PUBLIC_PATH>/bundle/stash.yaml?sources=...
|
||||||
|
HEAD /<PUBLIC_PATH>/bundle/stash.yaml?sources=...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 当前默认配置文件含义
|
||||||
|
|
||||||
|
### 6.1 `config/sources.yaml`
|
||||||
|
|
||||||
|
这是目前最核心的配置文件,已经承载了:
|
||||||
|
|
||||||
|
- `public_path`
|
||||||
|
- `sources`
|
||||||
|
- `regions`
|
||||||
|
- `rules`
|
||||||
|
- `clients`
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
#### `sources`
|
||||||
|
定义机场源:
|
||||||
|
|
||||||
|
- `enabled`
|
||||||
|
- `display_name`
|
||||||
|
- `kind`
|
||||||
|
- `url`
|
||||||
|
- `prefix`
|
||||||
|
- `include_regex`
|
||||||
|
- `exclude_regex`
|
||||||
|
|
||||||
|
当前 `kind` 主要按 `clash_yaml` 用。
|
||||||
|
|
||||||
|
#### `regions`
|
||||||
|
定义按节点名正则筛选的地区自动组。
|
||||||
|
|
||||||
|
#### `rules`
|
||||||
|
定义规则文件与它最终映射到哪个 policy。
|
||||||
|
|
||||||
|
#### `clients`
|
||||||
|
定义每个客户端类型的:
|
||||||
|
|
||||||
|
- provider 更新周期
|
||||||
|
- rule 更新周期
|
||||||
|
- test URL
|
||||||
|
- 主策略组名
|
||||||
|
- 自动组名
|
||||||
|
- 手动组名
|
||||||
|
- DIRECT 策略名
|
||||||
|
- 端口和 LAN 配置(Mihomo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 当前设计的主要思路
|
||||||
|
|
||||||
|
### 7.1 内部解耦,输出再组装
|
||||||
|
|
||||||
|
这是本项目最重要的原则。
|
||||||
|
|
||||||
|
不要直接维护最终配置文件;而是:
|
||||||
|
|
||||||
|
- 内部拆成源、规则、策略模板、客户端模板
|
||||||
|
- 对外按需求组合成 thin 或 bundle
|
||||||
|
|
||||||
|
这样未来无论是要做:
|
||||||
|
|
||||||
|
- 新客户端
|
||||||
|
- 新地区组
|
||||||
|
- 新业务组
|
||||||
|
- 新能力标签
|
||||||
|
- 新鉴权层
|
||||||
|
|
||||||
|
都不会把整个项目拖垮。
|
||||||
|
|
||||||
|
### 7.2 同时保留 thin 与 bundle
|
||||||
|
|
||||||
|
这不是重复劳动,而是为了满足不同客户端和不同使用习惯。
|
||||||
|
|
||||||
|
- thin 更适合 Mihomo / OpenClash
|
||||||
|
- bundle 更适合想“一条链接导入”的用户
|
||||||
|
- bundle 更方便把配额头暴露给客户端 UI
|
||||||
|
|
||||||
|
### 7.3 不在当前阶段过度抽象业务组
|
||||||
|
|
||||||
|
用户后来贴出了一份更复杂的目标配置,里面已经有:
|
||||||
|
|
||||||
|
- provider 选择
|
||||||
|
- 地区节点选择
|
||||||
|
- 业务策略组(Telegram / AI / YouTube / Netflix / Google / Microsoft / Apple / Game 等)
|
||||||
|
- 大量规则
|
||||||
|
|
||||||
|
但本次项目中并**没有**把那整套复杂业务组全部移植进来。
|
||||||
|
|
||||||
|
本次改造的重点不是“把规则抄完”,而是先把**后端架构**做对,避免继续堆在单文件 YAML 上。
|
||||||
|
|
||||||
|
换句话说,本次完成的是:
|
||||||
|
|
||||||
|
- 框架和接口层
|
||||||
|
- provider / bundle 双模式
|
||||||
|
- 配额头链路
|
||||||
|
- 基础策略组
|
||||||
|
|
||||||
|
而不是完整复刻用户那份业务规则大配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 当前版本的局限
|
||||||
|
|
||||||
|
下面这些是明确存在、且后续值得继续做的地方。
|
||||||
|
|
||||||
|
### 8.1 只支持 Clash/Mihomo YAML 输入
|
||||||
|
|
||||||
|
还不支持:
|
||||||
|
|
||||||
|
- ss:// / vmess:// / vless:// / trojan:// 原始 URI 解析
|
||||||
|
- base64 聚合订阅解析
|
||||||
|
- sing-box 输入格式
|
||||||
|
- Surge / Loon 原生格式输入
|
||||||
|
|
||||||
|
### 8.2 `sources.yaml` 还承担了过多职责
|
||||||
|
|
||||||
|
后续建议继续拆分:
|
||||||
|
|
||||||
|
- `sources.yaml`
|
||||||
|
- `regions.yaml`
|
||||||
|
- `policies.yaml`
|
||||||
|
- `clients.yaml`
|
||||||
|
|
||||||
|
### 8.3 业务组还没有模板化
|
||||||
|
|
||||||
|
当前只有基础组:
|
||||||
|
|
||||||
|
- 机场选择
|
||||||
|
- 混合自动
|
||||||
|
- 手动切换
|
||||||
|
- 地区自动
|
||||||
|
- 节点选择
|
||||||
|
|
||||||
|
后续可以把用户想要的:
|
||||||
|
|
||||||
|
- `📲 电报消息`
|
||||||
|
- `💬 Ai平台`
|
||||||
|
- `📹 油管视频`
|
||||||
|
- `🎥 奈飞视频`
|
||||||
|
- `🌍 国外媒体`
|
||||||
|
- `📢 谷歌`
|
||||||
|
- `Ⓜ️ 微软服务`
|
||||||
|
- `🍎 苹果服务`
|
||||||
|
- `🎮 游戏平台`
|
||||||
|
|
||||||
|
抽象成 `policies.yaml` + 模板生成。
|
||||||
|
|
||||||
|
### 8.4 目前地区识别仅靠节点名正则
|
||||||
|
|
||||||
|
还没有做:
|
||||||
|
|
||||||
|
- 更可靠的地区标签系统
|
||||||
|
- 能力标签(Netflix / AI / Game / Native)
|
||||||
|
- 延迟或解锁能力探测
|
||||||
|
|
||||||
|
### 8.5 没有内置鉴权
|
||||||
|
|
||||||
|
当前项目更偏“后端核心能力”,安全性仍建议依赖前置层:
|
||||||
|
|
||||||
|
- Caddy / Nginx Basic Auth
|
||||||
|
- 仅 Tailscale 可访问
|
||||||
|
- 或其他反代鉴权方案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 后续最值得继续做的事
|
||||||
|
|
||||||
|
优先级建议如下。
|
||||||
|
|
||||||
|
### P1:把用户目标配置里的“业务组模板”抽出来
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
- `policies.yaml`
|
||||||
|
|
||||||
|
做法:
|
||||||
|
|
||||||
|
- 把业务组可选项抽成模板
|
||||||
|
- 让规则只引用稳定业务组名
|
||||||
|
- 让机场组 / 地区组 / 自动组作为底层构件
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
让项目真正实现“provider 选择 → 节点选择 → 业务规则指向”。
|
||||||
|
|
||||||
|
### P1:把地区配置独立出来
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
- `regions.yaml`
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 减少 `sources.yaml` 复杂度
|
||||||
|
- 地区过滤可以单独维护
|
||||||
|
|
||||||
|
### P1:支持原始订阅解析
|
||||||
|
|
||||||
|
新增:
|
||||||
|
|
||||||
|
- parser 层
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 直接吃机场原始 URI / base64 订阅
|
||||||
|
- 降低对外部转换器的依赖
|
||||||
|
|
||||||
|
### P2:支持能力标签
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- `netflix: true`
|
||||||
|
- `ai: true`
|
||||||
|
- `game: true`
|
||||||
|
- `native: true`
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 自动生成 `🎥 奈飞节点`、`💬 AI可用节点` 之类的能力池
|
||||||
|
|
||||||
|
### P2:缓存和探测增强
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- provider 拉取加 TTL / ETag / 条件请求
|
||||||
|
- 节点能力探测可选化
|
||||||
|
- 避免每次请求都全量重拉上游
|
||||||
|
|
||||||
|
### P3:更强的模板体系
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 区分 Mihomo / Stash / OpenClash 细节
|
||||||
|
- 更好地控制 bundle 与 thin 差异
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 给 Codex 的工作建议
|
||||||
|
|
||||||
|
如果 Codex 要继续接手,建议按下面顺序继续,不要直接推翻当前结构。
|
||||||
|
|
||||||
|
### 建议 1
|
||||||
|
先保留当前 `services/` 分层,不要把逻辑重新打回 `main.py`。
|
||||||
|
|
||||||
|
### 建议 2
|
||||||
|
优先做 `policies.yaml`,把用户贴出来的复杂业务组迁进模板体系。
|
||||||
|
|
||||||
|
### 建议 3
|
||||||
|
不要急着做“多机场流量合并显示”,继续坚持“只取第一个源”的策略,除非产品需求明确变更。
|
||||||
|
|
||||||
|
### 建议 4
|
||||||
|
原始订阅解析应作为单独模块实现,不要污染当前 provider / profile builder。
|
||||||
|
|
||||||
|
### 建议 5
|
||||||
|
如果将来要支持更多客户端,优先抽象:
|
||||||
|
|
||||||
|
- 输入解析层
|
||||||
|
- 中间节点模型
|
||||||
|
- 输出模板层
|
||||||
|
|
||||||
|
而不是在 builder 里堆条件分支。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 一句话总结
|
||||||
|
|
||||||
|
这个项目的本质不是“再写一个 subconverter”,而是:
|
||||||
|
|
||||||
|
> **把多机场源、规则、策略组和客户端输出方式解耦维护,再按客户端需要动态组装成 thin 配置或 bundle 配置,并把第一个源的订阅配额头继续透传给客户端。**
|
||||||
|
|
||||||
|
这就是当前这版优化的核心目标,也是后续继续演进时不应该偏离的方向。
|
||||||
Reference in New Issue
Block a user