diff --git a/README.md b/README.md index 2d54a0b..2100db4 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ sub-provider/ rules.py subscriptions.py config/ + app.yaml + clients.yaml + policy-groups.yaml + regions.yaml + rules.yaml sources.yaml rules/ reject.yaml @@ -73,13 +78,22 @@ cp .env.example .env - 也可以直接填本地文件路径,例如 `/app/data/sources/airport-b.txt` 或 `file:///app/data/sources/airport-b.txt` - 允许把其中一个留空;留空时这个机场会自动跳过 -3. 启动: +3. 按需调整 `config/` 下拆分后的配置文件: + +- `app.yaml`:公共访问路径 +- `sources.yaml`:机场源配置 +- `regions.yaml`:地区自动组 +- `policy-groups.yaml`:选择器组和业务策略组 +- `rules.yaml`:规则绑定 +- `clients.yaml`:客户端模板 + +4. 启动: ```bash docker compose up -d --build ``` -4. 访问检查: +5. 访问检查: - 健康检查:`http://YOUR_HOST:18080/healthz` - 单 provider: diff --git a/app/config.py b/app/config.py index 5ef33b6..ecb0087 100644 --- a/app/config.py +++ b/app/config.py @@ -27,7 +27,13 @@ class Settings(BaseSettings): max_proxy_name_length: int = 80 default_user_agent: str = "sub-provider/0.2" + config_dir: Path = CONFIG_DIR sources_file: Path = CONFIG_DIR / "sources.yaml" + app_config_file: Path = CONFIG_DIR / "app.yaml" + clients_file: Path = CONFIG_DIR / "clients.yaml" + regions_file: Path = CONFIG_DIR / "regions.yaml" + policy_groups_file: Path = CONFIG_DIR / "policy-groups.yaml" + rules_config_file: Path = CONFIG_DIR / "rules.yaml" rules_dir: Path = CONFIG_DIR / "rules" bundle_cache_dir: Path = ROOT_DIR / "output" / "bundle-cache" fetch_cache_dir: Path = DATA_DIR / "fetch-cache" diff --git a/app/services/loader.py b/app/services/loader.py index dd12bbd..edac2f5 100644 --- a/app/services/loader.py +++ b/app/services/loader.py @@ -3,6 +3,7 @@ from __future__ import annotations import os import re from pathlib import Path +from typing import Any import yaml @@ -21,7 +22,44 @@ def _expand_env(value): return value -def load_app_config(path: Path) -> AppConfig: +def _load_yaml(path: Path) -> dict[str, Any]: + if not path.is_file(): + return {} raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {} - expanded = _expand_env(raw) + if not isinstance(raw, dict): + raise ValueError(f"Config file must contain a top-level mapping: {path}") + return raw + + +def _section_value(raw: dict[str, Any], section: str) -> Any: + if section in raw: + return raw[section] + return raw + + +def _load_split_config(path: Path) -> dict[str, Any]: + config_dir = path if path.is_dir() else path.parent + legacy_raw = _load_yaml(path) if path.is_file() else {} + + app_raw = _load_yaml(config_dir / "app.yaml") + sources_raw = _load_yaml(config_dir / "sources.yaml") + regions_raw = _load_yaml(config_dir / "regions.yaml") + groups_raw = _load_yaml(config_dir / "policy-groups.yaml") + rules_raw = _load_yaml(config_dir / "rules.yaml") + clients_raw = _load_yaml(config_dir / "clients.yaml") + + merged: dict[str, Any] = {} + merged.update({key: value for key, value in legacy_raw.items() if key not in AppConfig.model_fields}) + merged["public_path"] = app_raw.get("public_path", legacy_raw.get("public_path")) + merged["sources"] = _section_value(sources_raw, "sources") if sources_raw else legacy_raw.get("sources", {}) + merged["regions"] = _section_value(regions_raw, "regions") if regions_raw else legacy_raw.get("regions", {}) + merged["selector_groups"] = groups_raw.get("selector_groups", legacy_raw.get("selector_groups", [])) + merged["policy_groups"] = groups_raw.get("policy_groups", legacy_raw.get("policy_groups", [])) + merged["rules"] = _section_value(rules_raw, "rules") if rules_raw else legacy_raw.get("rules", {}) + merged["clients"] = _section_value(clients_raw, "clients") if clients_raw else legacy_raw.get("clients", {}) + return merged + + +def load_app_config(path: Path) -> AppConfig: + expanded = _expand_env(_load_split_config(path)) return AppConfig.model_validate(expanded) diff --git a/config/app.yaml b/config/app.yaml new file mode 100644 index 0000000..0a45c10 --- /dev/null +++ b/config/app.yaml @@ -0,0 +1 @@ +public_path: ${PUBLIC_PATH} diff --git a/config/clients.yaml b/config/clients.yaml new file mode 100644 index 0000000..899fce0 --- /dev/null +++ b/config/clients.yaml @@ -0,0 +1,33 @@ +clients: + mihomo: + title: HomeLab Mihomo + provider_interval: 21600 + rule_interval: 86400 + test_url: https://www.gstatic.com/generate_204 + test_interval: 300 + main_policy: 🚀 节点选择 + source_policy: ☁️ 机场选择 + mixed_auto_policy: ♻️ 自动选择 + manual_policy: 🚀 手动切换 + direct_policy: DIRECT + mode: rule + allow_lan: true + ipv6: true + mixed_port: 7890 + socks_port: 7891 + log_level: info + + stash: + title: HomeLab Stash + provider_interval: 21600 + rule_interval: 86400 + test_url: https://www.gstatic.com/generate_204 + test_interval: 300 + main_policy: 🚀 节点选择 + source_policy: ☁️ 机场选择 + mixed_auto_policy: ♻️ 自动选择 + manual_policy: 🚀 手动切换 + direct_policy: DIRECT + mode: rule + ipv6: true + log_level: info diff --git a/config/policy-groups.yaml b/config/policy-groups.yaml new file mode 100644 index 0000000..b74f532 --- /dev/null +++ b/config/policy-groups.yaml @@ -0,0 +1,180 @@ +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 }}" diff --git a/config/regions.yaml b/config/regions.yaml new file mode 100644 index 0000000..b1fa742 --- /dev/null +++ b/config/regions.yaml @@ -0,0 +1,25 @@ +regions: + hk: + name: "🇭🇰 香港节点" + filter: "(?i)(港|hk|hong kong|hongkong)" + tolerance: 50 + tw: + name: "🇨🇳 台湾节点" + filter: "(?i)(台|新北|彰化|tw|taiwan)" + tolerance: 50 + sg: + name: "🇸🇬 狮城节点" + filter: "(?i)(新加坡|坡|狮城|sg|singapore)" + tolerance: 50 + jp: + name: "🇯🇵 日本节点" + filter: "(?i)(日本|东京|大阪|埼玉|jp|japan)" + tolerance: 50 + us: + name: "🇺🇲 美国节点" + filter: "(?i)(美|波特兰|达拉斯|俄勒冈|凤凰城|费利蒙|硅谷|拉斯维加斯|洛杉矶|圣何塞|圣克拉拉|西雅图|芝加哥|us|united states)" + tolerance: 150 + kr: + name: "🇰🇷 韩国节点" + filter: "(?i)(kr|korea|kor|首尔|韩|韓)" + tolerance: 50 diff --git a/config/rules.yaml b/config/rules.yaml new file mode 100644 index 0000000..d628c13 --- /dev/null +++ b/config/rules.yaml @@ -0,0 +1,210 @@ +rules: + custom-proxy: + behavior: classical + format: text + policy: "{{ main_policy }}" + payload: + - DOMAIN-KEYWORD,cloudflare + - DOMAIN-KEYWORD,hetzner + - DOMAIN-KEYWORD,hdkylin + - DOMAIN-KEYWORD,steamusercontent + - DOMAIN-KEYWORD,steamcontent + - DOMAIN-KEYWORD,nintendo + - DOMAIN-KEYWORD,vscode + - DOMAIN-KEYWORD,btschool + + local-network: + file: acl4ssr/LocalAreaNetwork.yaml + behavior: classical + format: yaml + policy: "🎯 全球直连" + interval: 86400 + + unban: + file: acl4ssr/UnBan.yaml + behavior: classical + format: yaml + policy: "🎯 全球直连" + interval: 86400 + + reject: + file: acl4ssr/BanAD.yaml + behavior: classical + format: yaml + interval: 86400 + policy: "🛑 广告拦截" + + app-purify: + file: acl4ssr/BanProgramAD.yaml + behavior: classical + format: yaml + interval: 86400 + policy: "🍃 应用净化" + + google: + file: acl4ssr/Google.yaml + behavior: classical + format: yaml + policy: "📢 谷歌" + interval: 86400 + + google-cn: + file: acl4ssr/GoogleCN.yaml + behavior: classical + format: yaml + policy: "🎯 全球直连" + interval: 86400 + + steam-cn: + file: acl4ssr/SteamCN.yaml + behavior: classical + format: yaml + policy: "🎯 全球直连" + interval: 86400 + + microsoft-bing: + file: acl4ssr/Bing.yaml + behavior: classical + format: yaml + policy: "Ⓜ️ 微软Bing" + interval: 86400 + + microsoft-onedrive: + file: acl4ssr/OneDrive.yaml + behavior: classical + format: yaml + policy: "Ⓜ️ 微软云盘" + interval: 86400 + + microsoft: + file: acl4ssr/Microsoft.yaml + behavior: classical + format: yaml + policy: "Ⓜ️ 微软服务" + interval: 86400 + + apple: + file: acl4ssr/Apple.yaml + behavior: classical + format: yaml + policy: "🍎 苹果服务" + interval: 86400 + + telegram: + file: acl4ssr/Telegram.yaml + behavior: classical + format: yaml + policy: "📲 电报消息" + interval: 86400 + + ai: + file: acl4ssr/AI.yaml + behavior: classical + format: yaml + policy: "💬 Ai平台" + interval: 86400 + + openai: + file: acl4ssr/OpenAi.yaml + behavior: classical + format: yaml + policy: "💬 Ai平台" + interval: 86400 + + youtube: + file: acl4ssr/YouTube.yaml + behavior: classical + format: yaml + policy: "📹 油管视频" + interval: 86400 + + netflix: + file: acl4ssr/Netflix.yaml + behavior: classical + format: yaml + policy: "🎥 奈飞视频" + interval: 86400 + + proxy-media: + file: acl4ssr/ProxyMedia.yaml + behavior: classical + format: yaml + policy: "🌍 国外媒体" + interval: 86400 + + games-epic: + file: acl4ssr/Epic.yaml + behavior: classical + format: yaml + policy: "🎮 游戏平台" + interval: 86400 + + games-origin: + file: acl4ssr/Origin.yaml + behavior: classical + format: yaml + policy: "🎮 游戏平台" + interval: 86400 + + games-sony: + file: acl4ssr/Sony.yaml + behavior: classical + format: yaml + policy: "🎮 游戏平台" + interval: 86400 + + games-steam: + file: acl4ssr/Steam.yaml + behavior: classical + format: yaml + policy: "🎮 游戏平台" + interval: 86400 + + games-nintendo: + file: acl4ssr/Nintendo.yaml + behavior: classical + format: yaml + policy: "🎮 游戏平台" + interval: 86400 + + pt: + file: acl4ssr/PrivateTracker.yaml + behavior: classical + format: yaml + policy: "🎮 PT平台" + interval: 86400 + + cn-domain: + file: acl4ssr/ChinaDomain.yaml + behavior: classical + format: yaml + policy: "🎯 全球直连" + interval: 86400 + + cn-company-ip: + file: acl4ssr/ChinaCompanyIp.yaml + behavior: classical + format: yaml + policy: "🎯 全球直连" + interval: 86400 + + download: + file: acl4ssr/Download.yaml + behavior: classical + format: yaml + policy: "🎯 全球直连" + interval: 86400 + + proxy-gfw: + file: acl4ssr/ProxyGFWlist.yaml + behavior: classical + format: yaml + policy: "{{ main_policy }}" + interval: 86400 + + geoip-cn: + behavior: classical + format: text + policy: "🎯 全球直连" + payload: + - GEOIP,CN diff --git a/config/sources.yaml b/config/sources.yaml index b26a5f6..c2ef4ec 100644 --- a/config/sources.yaml +++ b/config/sources.yaml @@ -1,483 +1,28 @@ -public_path: ${PUBLIC_PATH} +airport-a: + enabled: true + display_name: A + kind: auto + url: ${AIRPORT_A_URL} + prefix: "[A] " + include_regex: "" + exclude_regex: "流量|重置|到期|续费|官网|离线|套餐" -sources: - airport-a: - enabled: true - display_name: A - kind: auto - url: ${AIRPORT_A_URL} - prefix: "[A] " - include_regex: "" - exclude_regex: "流量|重置|到期|续费|官网|离线|套餐" +airport-b: + enabled: true + display_name: B + kind: auto + url: ${AIRPORT_B_URL} + headers: + User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" + prefix: "[B] " + include_regex: "" + exclude_regex: "流量|重置|到期|续费|官网|离线|套餐" - airport-b: - enabled: true - display_name: B - kind: auto - url: ${AIRPORT_B_URL} - headers: - User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" - prefix: "[B] " - include_regex: "" - exclude_regex: "流量|重置|到期|续费|官网|离线|套餐" - - airport-c: - enabled: true - display_name: C - kind: auto - url: ${AIRPORT_C_URL} - prefix: "[C] " - include_regex: "" - exclude_regex: "流量|重置|到期|续费|官网|离线|套餐" - -regions: - hk: - name: "🇭🇰 香港节点" - filter: "(?i)(港|hk|hong kong|hongkong)" - tolerance: 50 - tw: - name: "🇨🇳 台湾节点" - filter: "(?i)(台|新北|彰化|tw|taiwan)" - tolerance: 50 - sg: - name: "🇸🇬 狮城节点" - filter: "(?i)(新加坡|坡|狮城|sg|singapore)" - tolerance: 50 - jp: - name: "🇯🇵 日本节点" - filter: "(?i)(日本|东京|大阪|埼玉|jp|japan)" - tolerance: 50 - us: - name: "🇺🇲 美国节点" - filter: "(?i)(美|波特兰|达拉斯|俄勒冈|凤凰城|费利蒙|硅谷|拉斯维加斯|洛杉矶|圣何塞|圣克拉拉|西雅图|芝加哥|us|united states)" - 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: - custom-proxy: - behavior: classical - format: text - policy: "{{ main_policy }}" - payload: - - DOMAIN-KEYWORD,cloudflare - - DOMAIN-KEYWORD,hetzner - - DOMAIN-KEYWORD,hdkylin - - DOMAIN-KEYWORD,steamusercontent - - DOMAIN-KEYWORD,steamcontent - - DOMAIN-KEYWORD,nintendo - - DOMAIN-KEYWORD,vscode - - DOMAIN-KEYWORD,btschool - - local-network: - file: acl4ssr/LocalAreaNetwork.yaml - behavior: classical - format: yaml - policy: "🎯 全球直连" - interval: 86400 - - unban: - file: acl4ssr/UnBan.yaml - behavior: classical - format: yaml - policy: "🎯 全球直连" - interval: 86400 - - reject: - file: acl4ssr/BanAD.yaml - behavior: classical - format: yaml - interval: 86400 - policy: "🛑 广告拦截" - - app-purify: - file: acl4ssr/BanProgramAD.yaml - behavior: classical - format: yaml - interval: 86400 - policy: "🍃 应用净化" - - google: - file: acl4ssr/Google.yaml - behavior: classical - format: yaml - policy: "📢 谷歌" - interval: 86400 - - google-cn: - file: acl4ssr/GoogleCN.yaml - behavior: classical - format: yaml - policy: "🎯 全球直连" - interval: 86400 - - steam-cn: - file: acl4ssr/SteamCN.yaml - behavior: classical - format: yaml - policy: "🎯 全球直连" - interval: 86400 - - microsoft-bing: - file: acl4ssr/Bing.yaml - behavior: classical - format: yaml - policy: "Ⓜ️ 微软Bing" - interval: 86400 - - microsoft-onedrive: - file: acl4ssr/OneDrive.yaml - behavior: classical - format: yaml - policy: "Ⓜ️ 微软云盘" - interval: 86400 - - microsoft: - file: acl4ssr/Microsoft.yaml - behavior: classical - format: yaml - policy: "Ⓜ️ 微软服务" - interval: 86400 - - apple: - file: acl4ssr/Apple.yaml - behavior: classical - format: yaml - policy: "🍎 苹果服务" - interval: 86400 - - telegram: - file: acl4ssr/Telegram.yaml - behavior: classical - format: yaml - policy: "📲 电报消息" - interval: 86400 - - ai: - file: acl4ssr/AI.yaml - behavior: classical - format: yaml - policy: "💬 Ai平台" - interval: 86400 - - openai: - file: acl4ssr/OpenAi.yaml - behavior: classical - format: yaml - policy: "💬 Ai平台" - interval: 86400 - - youtube: - file: acl4ssr/YouTube.yaml - behavior: classical - format: yaml - policy: "📹 油管视频" - interval: 86400 - - netflix: - file: acl4ssr/Netflix.yaml - behavior: classical - format: yaml - policy: "🎥 奈飞视频" - interval: 86400 - - proxy-media: - file: acl4ssr/ProxyMedia.yaml - behavior: classical - format: yaml - policy: "🌍 国外媒体" - interval: 86400 - - games-epic: - file: acl4ssr/Epic.yaml - behavior: classical - format: yaml - policy: "🎮 游戏平台" - interval: 86400 - - games-origin: - file: acl4ssr/Origin.yaml - behavior: classical - format: yaml - policy: "🎮 游戏平台" - interval: 86400 - - games-sony: - file: acl4ssr/Sony.yaml - behavior: classical - format: yaml - policy: "🎮 游戏平台" - interval: 86400 - - games-steam: - file: acl4ssr/Steam.yaml - behavior: classical - format: yaml - policy: "🎮 游戏平台" - interval: 86400 - - games-nintendo: - file: acl4ssr/Nintendo.yaml - behavior: classical - format: yaml - policy: "🎮 游戏平台" - interval: 86400 - - pt: - file: acl4ssr/PrivateTracker.yaml - behavior: classical - format: yaml - policy: "🎮 PT平台" - interval: 86400 - - cn-domain: - file: acl4ssr/ChinaDomain.yaml - behavior: classical - format: yaml - policy: "🎯 全球直连" - interval: 86400 - - cn-company-ip: - file: acl4ssr/ChinaCompanyIp.yaml - behavior: classical - format: yaml - policy: "🎯 全球直连" - interval: 86400 - - download: - file: acl4ssr/Download.yaml - behavior: classical - format: yaml - policy: "🎯 全球直连" - interval: 86400 - - proxy-gfw: - file: acl4ssr/ProxyGFWlist.yaml - behavior: classical - format: yaml - policy: "{{ main_policy }}" - interval: 86400 - - geoip-cn: - behavior: classical - format: text - policy: "🎯 全球直连" - payload: - - GEOIP,CN - -clients: - mihomo: - title: HomeLab Mihomo - provider_interval: 21600 - rule_interval: 86400 - test_url: https://www.gstatic.com/generate_204 - test_interval: 300 - main_policy: 🚀 节点选择 - source_policy: ☁️ 机场选择 - mixed_auto_policy: ♻️ 自动选择 - manual_policy: 🚀 手动切换 - direct_policy: DIRECT - mode: rule - allow_lan: true - ipv6: true - mixed_port: 7890 - socks_port: 7891 - log_level: info - - stash: - title: HomeLab Stash - provider_interval: 21600 - rule_interval: 86400 - test_url: https://www.gstatic.com/generate_204 - test_interval: 300 - main_policy: 🚀 节点选择 - source_policy: ☁️ 机场选择 - mixed_auto_policy: ♻️ 自动选择 - manual_policy: 🚀 手动切换 - direct_policy: DIRECT - mode: rule - ipv6: true - log_level: info +airport-c: + enabled: true + display_name: C + kind: auto + url: ${AIRPORT_C_URL} + prefix: "[C] " + include_regex: "" + exclude_regex: "流量|重置|到期|续费|官网|离线|套餐" diff --git a/docs/分阶段重构方案.md b/docs/分阶段重构方案.md new file mode 100644 index 0000000..15d635c --- /dev/null +++ b/docs/分阶段重构方案.md @@ -0,0 +1,686 @@ +# sub-provider 分阶段重构方案 + +## 1. 文档目标 + +这份文档不是重新定义“最终想做成什么”,而是基于当前 `sub-provider` 已有实现,给出一版可逐步落地、每阶段都能交付可运行结果的重构路径。 + +当前项目已经具备以下基础能力: + +* 读取 `config/sources.yaml` +* 拉取和缓存上游订阅 +* 解析 Clash YAML / base64 URI / 明文 URI 订阅 +* 合并节点并生成 bundle YAML +* 生成 thin client 配置 +* 加载本地规则文件并拼接 `rules` +* 透传第一个源的 `Subscription-Userinfo` + +因此重构的目标不是“从零搭系统”,而是把现有静态配置驱动的生成器,逐步改造成: + +* 内部模块化更清晰 +* 配置存储可演进到数据库 +* 对外仍以完整 YAML 为主 +* 保留现有已验证能力 +* 后续可平滑加管理页和快照系统 + +--- + +## 2. 总体原则 + +### 原则 1:对外接口尽量稳定 + +在重构前几阶段,尽量不打断现有输出能力: + +* `/bundle/{client}.yaml` +* `/clients/{client}.yaml` +* `/providers/{name}.yaml` +* `/providers/merged.yaml` + +即使最终主路线偏向 bundle,也不建议在早期重构中直接删除 thin/provider 能力。 + +### 原则 2:先拆“配置来源”,再拆“表现层” + +当前项目的核心问题不是生成逻辑完全不可用,而是配置全部堆在 `sources.yaml` 里。 +优先级应是: + +1. 把配置从单一 YAML 拆到更清晰的数据结构 +2. 再引入数据库承载配置 +3. 最后再补管理页面 + +不要一上来把大量时间花在页面上。 + +### 原则 3:规则正文继续文件化 + +规则文件仍然保存在项目目录中,继续使用 Git 管理。 +数据库只存: + +* 规则模块元数据 +* 规则启用关系 +* 规则顺序 +* 规则绑定到哪个策略组 + +不把 ACL4SSR 规则逐条拆进数据库。 + +### 原则 4:保留动态选源能力 + +当前项目支持通过 `?sources=` 在请求级动态组合源,这个能力实际很有价值。 +重构后不建议退化成只能在 profile 中静态绑定源。 + +建议模型上同时支持: + +* profile 默认源集合 +* 请求级 `sources` override + +--- + +## 3. 对现有项目的基线判断 + +### 3.1 现状优点 + +当前代码结构虽然还偏轻量,但核心职责已经初步分开: + +* `subscriptions.py` 负责源抓取、解析、节点转换 +* `profiles.py` 负责 thin / bundle 配置组装 +* `rules.py` 负责规则文件加载和规则输出 +* `bundle_cache.py` / `fetch_cache.py` 负责磁盘缓存 + +这意味着重构时应该优先“抽象和替换内部数据来源”,而不是推倒服务层。 + +### 3.2 现状主要问题 + +当前主要限制有: + +* `sources.yaml` 同时承载源配置、地区分组、业务策略组、规则绑定、客户端配置 +* 配置模型偏静态,不利于面板管理 +* bundle 缓存仍是 TTL 模式,不是内容指纹模式 +* 没有配置快照、生成快照、审计能力 +* 当前没有数据库层,也没有迁移体系 + +### 3.3 结论 + +最稳的路线不是“大重写”,而是: + +1. 保持现有生成骨架 +2. 把配置与渲染逻辑进一步解耦 +3. 再把配置落进数据库 +4. 最后加面板和快照 + +--- + +## 4. 目标架构 + +推荐演进成如下结构: + +```text +请求 + -> API 路由 + -> Profile 解析 + -> Source 选择 + -> Source 拉取 / 缓存 + -> 节点解析 / 标准化 / 处理 + -> 策略组装配 + -> 规则模块解析 + -> 完整 YAML 渲染 + -> 结果缓存 / 快照 + -> 返回响应头与 YAML +``` + +配置来源则逐步从: + +```text +sources.yaml +``` + +演进为: + +```text +数据库配置 + 本地规则资产文件 + 少量默认 YAML 模板 +``` + +--- + +## 5. 分阶段路线 + +建议拆成五个阶段,每个阶段结束都能保留一个稳定可运行版本。 + +### 阶段 0:重构准备与基线冻结 + +目标: + +* 不改行为,先明确现有能力边界 +* 为后续重构建立可回归的基线 + +本阶段任务: + +1. 盘点当前公开接口、查询参数、响应头语义 +2. 记录当前 `sources.yaml` 中各配置块的职责 +3. 为核心生成路径补最小测试 +4. 输出一份“当前行为基线”文档 + +验收标准: + +* 至少覆盖以下场景: + * 单源 provider + * 多源 merged provider + * bundle 输出 + * thin 输出 + * 第一个源配额头透传 +* 有一组固定输入可验证当前输出结构不被意外破坏 + +建议产物: + +* `tests/` 下最小回归测试 +* `docs/当前行为基线.md` + +说明: + +这一阶段很重要。没有基线,后面数据库化后很难判断是“重构带来的结构变化”,还是“偷偷改了行为”。 + +--- + +### 阶段 1:配置解耦,但仍以 YAML 为配置源 + +目标: + +* 先把“一个巨大的 `sources.yaml`”拆成多个清晰模块 +* 暂时不引入数据库 +* 不改变线上运行方式 + +本阶段任务: + +1. 拆分当前 `sources.yaml`: + * `config/sources.yaml` + * `config/clients.yaml` + * `config/rule-bindings.yaml` + * `config/policy-groups.yaml` + * `config/regions.yaml` +2. 新增统一配置加载层,把多个 YAML 聚合成当前运行所需的内部配置对象 +3. 把当前 `AppConfig` 进一步拆开,减少“大一统模型” +4. 让 `main.py` 不再直接依赖单个全局 `app_config` +5. 把“配置加载”和“配置解释”职责从“服务逻辑”中剥离 + +建议目录演进: + +```text +app/ + config.py + main.py + models/ + source.py + client.py + profile.py + rule.py + group.py + services/ + config_loader.py + subscriptions.py + profiles.py + rules.py +``` + +验收标准: + +* 对外接口路径和行为保持不变 +* `bundle` 与 `thin` 输出内容结构基本一致 +* 已不再依赖单个巨型 YAML 文件 + +这一阶段收益: + +* 先把系统结构理顺 +* 后续接数据库时,只需要替换配置仓储层,不需要大改生成器本身 + +--- + +### 阶段 2:引入数据库配置层,保留 YAML 兼容导入 + +目标: + +* 让配置从静态文件迁移到数据库 +* 但仍支持从 YAML 导入初始数据 + +本阶段任务: + +1. 引入 SQLAlchemy 与 Alembic +2. 增加 `DATABASE_URL` +3. 建立第一批核心表 +4. 实现配置仓储层 +5. 提供 YAML 初始化导入脚本 + +推荐第一批表: + +* `sources` +* `profiles` +* `profile_sources` +* `rule_modules` +* `profile_rule_modules` +* `policy_groups` +* `profile_overrides` + +推荐暂不引入的表: + +* `generated_artifacts` +* `sync_logs` +* `rule_module_versions` + +原因: + +先把“当前运行配置”迁过去,比一开始就把快照、审计、版本全部做全更稳。 + +#### 推荐字段设计 + +`sources` + +* `id` +* `key` +* `name` +* `enabled` +* `kind` +* `url` +* `display_name` +* `headers_json` +* `include_regex` +* `exclude_regex` +* `prefix` +* `suffix` +* `cache_ttl_seconds` +* `created_at` +* `updated_at` + +`profiles` + +* `id` +* `key` +* `name` +* `client_type` +* `description` +* `enabled` +* `allow_lan` +* `ipv6` +* `mixed_port` +* `socks_port` +* `mode` +* `log_level` +* `main_policy` +* `source_policy` +* `mixed_auto_policy` +* `manual_policy` +* `direct_policy` +* `test_url` +* `test_interval` +* `provider_interval` +* `rule_interval` +* `created_at` +* `updated_at` + +`profile_sources` + +* `id` +* `profile_id` +* `source_id` +* `order_index` +* `enabled` + +`rule_modules` + +* `id` +* `key` +* `name` +* `description` +* `file_path` +* `behavior` +* `format` +* `default_policy` +* `default_no_resolve` +* `default_payload_json` +* `category` +* `created_at` +* `updated_at` + +`profile_rule_modules` + +* `id` +* `profile_id` +* `rule_module_id` +* `enabled` +* `order_index` +* `policy_override` +* `payload_override_json` +* `no_resolve_override` + +`policy_groups` + +* `id` +* `profile_id` +* `name` +* `group_kind` +* `type` +* `order_index` +* `filter_regex` +* `proxies_json` +* `url` +* `interval` +* `tolerance` +* `enabled` + +这里的 `group_kind` 建议至少支持: + +* `static` +* `filter` +* `generated` + +这比单纯用一个 `config_json` 更适合当前项目,因为现有组装里同时存在静态组和按 regex 选节点的组。 + +#### 仓储层建议 + +新增仓储抽象,例如: + +* `SourceRepository` +* `ProfileRepository` +* `RuleModuleRepository` +* `PolicyGroupRepository` + +让生成服务只依赖仓储接口,而不是依赖数据库实现细节。 + +验收标准: + +* 使用数据库也能生成与阶段 1 基本一致的 bundle/thin 配置 +* 可以从原 YAML 导入初始配置 +* 切换配置来源时,渲染层无需大改 + +--- + +### 阶段 3:渲染流水线模块化与输出主路线收敛 + +目标: + +* 把“源处理、规则处理、组装渲染”彻底做成稳定流水线 +* 明确 bundle 是主输出路线 +* thin/provider 进入兼容保留状态 + +本阶段任务: + +1. 拆分服务层职责: + * `source_fetcher.py` + * `source_parser.py` + * `proxy_processor.py` + * `policy_group_builder.py` + * `rule_resolver.py` + * `profile_renderer.py` +2. 引入统一内部节点模型 +3. 引入统一内部“已解析 profile”模型 +4. 将当前 `build_bundle_profile()` 的逻辑拆成多步处理 +5. 清理当前 token 展开逻辑,明确保留哪些模板 token + +#### 内部模型建议 + +不要把节点模型收得太死。建议: + +```python +class ProxyNode(BaseModel): + name: str + type: str + server: str | None = None + port: int | None = None + udp: bool = True + tags: list[str] = Field(default_factory=list) + attrs: dict[str, Any] = Field(default_factory=dict) +``` + +其中: + +* 通用字段单独保留 +* 协议特有字段放 `attrs` +* 最终输出时再按协议合并回 YAML 结构 + +这样比单纯的 `raw` 更可控。 + +#### 规则系统建议 + +这一阶段不要急着把所有规则文件改成 `{{ target_group }}` 模板格式。 +先保留当前模式: + +* 文件内容仍是 payload +* policy 在 profile 绑定关系上决定 + +原因: + +* 当前 thin 模式的 `rule-providers` 仍依赖 payload 文件输出 +* 先保留兼容性,后面再决定是否升级规则文件模板化 + +验收标准: + +* bundle 渲染主路径清晰可测 +* 服务层之间依赖方向清楚 +* 配置仓储和渲染器分离 + +--- + +### 阶段 4:管理接口与最小面板 + +目标: + +* 提供最小配置管理能力 +* 不做复杂前后端分离 + +本阶段任务: + +1. 增加管理 API: + * source CRUD + * profile CRUD + * rule module 查看与 profile 绑定管理 + * policy group 查看与编辑 +2. 增加预览接口 +3. 增加最小页面: + * sources + * profiles + * rule bindings + * preview +4. 提供 profile 级 bundle 下载入口 + +建议接口: + +* `GET /api/sources` +* `POST /api/sources` +* `PUT /api/sources/{id}` +* `GET /api/profiles` +* `POST /api/profiles` +* `PUT /api/profiles/{id}` +* `GET /api/profiles/{id}/rules` +* `PUT /api/profiles/{id}/rules` +* `GET /api/profiles/{id}/groups` +* `PUT /api/profiles/{id}/groups` +* `POST /api/profiles/{id}/preview` +* `GET /profiles/{key}/bundle.yaml` + +面板技术建议: + +* Jinja2 +* HTMX 或最少量原生 JS + +不建议此时做: + +* React/Vue 前后端分离 +* 复杂权限系统 +* 在线全文规则编辑器 + +验收标准: + +* 不改代码即可新增一个 source +* 不改 YAML 文件即可调整 profile 规则顺序 +* 页面可直接预览 bundle 输出 + +--- + +### 阶段 5:快照、缓存升级与可追踪性 + +目标: + +* 从“能生成”升级到“可追踪、可复现、可排查” + +本阶段任务: + +1. 增加 `generated_artifacts` +2. 增加内容指纹缓存 +3. 增加源同步状态记录 +4. 增加简单 diff 能力 +5. 增加错误展示 + +推荐新增表: + +`generated_artifacts` + +* `id` +* `profile_id` +* `request_sources_json` +* `content` +* `content_hash` +* `source_hash` +* `rules_hash` +* `profile_hash` +* `headers_json` +* `generated_at` + +`source_sync_logs` + +* `id` +* `source_id` +* `status` +* `error_message` +* `response_headers_json` +* `content_hash` +* `created_at` + +#### 缓存策略建议 + +当前项目 bundle 缓存是 TTL 模式。后续应逐步改为: + +* fetch cache: TTL +* parsed snapshot cache: 源内容 hash +* bundle cache: `source_hash + rules_hash + profile_hash + request_sources` + +这样才能真正做到: + +* 配置没变就稳定复用 +* 配置变了就精准失效 + +验收标准: + +* 能查看某个 profile 最近几次生成记录 +* 能知道本次 bundle 为什么重新生成 +* 能追踪某个源最近一次同步是否失败 + +--- + +## 6. 推荐实施顺序 + +如果按投入产出比排序,建议实际开发顺序如下: + +1. 阶段 0 +2. 阶段 1 +3. 阶段 2 +4. 阶段 3 +5. 阶段 4 +6. 阶段 5 + +其中真正的“第一版可交付里程碑”建议定在阶段 2 结束时。 + +因为到了阶段 2,已经具备: + +* 数据库存储配置 +* 可继续生成完整 YAML +* 配置不再绑死在单文件 YAML 中 + +这时即使还没有面板,系统内核已经完成了最关键升级。 + +--- + +## 7. 每阶段风险点 + +### 阶段 1 风险 + +风险: + +* 拆 YAML 时容易出现字段兼容问题 + +控制方式: + +* 先保留兼容加载层 +* 老配置格式在一段时间内继续可读 + +### 阶段 2 风险 + +风险: + +* 数据建模过度,导致表过多、实现过重 + +控制方式: + +* 第一批只建运行必需表 +* 快照与日志延后 + +### 阶段 3 风险 + +风险: + +* 过早重写规则系统,导致 thin 模式兼容性下降 + +控制方式: + +* 先保留 payload 模式 +* 规则模板化放后续增量处理 + +### 阶段 4 风险 + +风险: + +* 过早投入页面开发,拖慢主流程 + +控制方式: + +* 先 API 后页面 +* 页面只做最小可用 + +### 阶段 5 风险 + +风险: + +* 快照和缓存逻辑做得太复杂,维护成本上升 + +控制方式: + +* 先做内容哈希与生成记录 +* diff 与审计能力逐步补 + +--- + +## 8. 明确不建议的做法 + +1. 不建议一开始就删除当前 thin/provider 能力 +2. 不建议一开始就把所有规则文件模板化 +3. 不建议把 profile 的源集合只做成一个 `source_ids_json` +4. 不建议把策略组全塞进一个不透明 `config_json` +5. 不建议先做复杂前端再补业务内核 +6. 不建议先做快照系统再做数据库配置层 + +--- + +## 9. 建议的第一批实际改动 + +如果下一步开始真正动代码,建议先做这几件事: + +1. 新建阶段 0 的测试和基线文档 +2. 拆分 `sources.yaml`,完成阶段 1 +3. 引入 SQLAlchemy/Alembic 和最小表结构 +4. 增加 YAML -> DB 导入脚本 +5. 让 bundle 渲染从数据库读取 profile / source / rules 配置 + +这五步完成后,再去做页面,整体节奏会稳很多。 + +--- + +## 10. 一句话结论 + +`sub-provider` 的重构最稳路线是: + +> 先做配置解耦,再做数据库配置层,再做渲染流水线固化,最后补管理页面和快照系统。 + +这样每一阶段都能保留一个可运行版本,也最符合当前项目已经有代码基础的现实情况。 diff --git a/docs/改造方案.md b/docs/改造方案.md new file mode 100644 index 0000000..f6421fc --- /dev/null +++ b/docs/改造方案.md @@ -0,0 +1,976 @@ +下面这份你可以直接丢给 Codex。 +我把我们刚刚聊出来的**最终拍板结论**和**完整实现方案**都整理进去了。 + +--- + +# sub-provider 重构方案(最终版,完整 YAML 路线) + +## 1. 最终结论 + +本项目后续重构,采用以下最终选择: + +### 对外输出 + +* **统一输出单个完整 YAML** +* 不强依赖客户端支持 `rule-providers` / `proxy-providers` +* 目标是优先保证: + + * 兼容性 + * 可调试性 + * 易分发 + * 易维护 + +### 对内实现 + +* 内部仍然采用**模块化**设计 +* 在 sub-provider 内部完成: + + * 源拉取与缓存 + * 节点解析与标准化 + * 节点筛选、重命名、去重 + * 规则模块组合 + * 策略组装配 + * DNS / TUN / 通用配置拼装 + * 最终完整 YAML 渲染 + +### 规则存储方式 + +* **规则正文继续放项目文件** +* 采用类似 ACL4SSR 的维护方式: + + * 一个类别的规则放一个文件 + * 规则文件便于 Git 管理、diff、回滚、手工维护 +* 不把 ACL4SSR 大量规则逐条入库 + +### 规则组合与面板配置 + +* **规则的组合关系、启用状态、绑定的策略组、顺序、参数等放 SQLite** +* 面板配置的是: + + * 选哪些规则模块 + * 每个模块绑定哪个策略组 + * 模块顺序 + * 模块参数 + * profile 配置 + * 源配置 + * 少量 prepend / append 自定义规则 + +### 数据库选择 + +* **默认数据库:SQLite** +* 原因: + + * 当前项目是单服务、轻量配置中心、生成器型应用 + * SQLite 部署和迁移成本最低 + * 最适合当前阶段快速落地 +* 代码层要保留未来切 PostgreSQL 的能力: + + * 使用 SQLAlchemy + * 使用 Alembic + * 用 `DATABASE_URL` 配置数据库连接 +* 当前不推荐 Mongo 作为主库 +* 当前不优先推荐 MySQL + +--- + +## 2. 目标 + +将现有 Python sub-provider 项目重构为一个: + +* 可面板管理 +* 配置解耦 +* 规则模块化 +* 输出完整 YAML +* 可缓存 +* 可预览 +* 可追踪生成结果 + +的配置生成系统。 + +--- + +## 3. 项目边界 + +### 本阶段要做 + +1. 面板化管理订阅源 +2. 规则模块化 +3. 规则组合关系数据库化 +4. 支持多个输出 Profile +5. 服务端拼装生成完整 YAML +6. 做缓存和快照 +7. 支持预览和下载 + +### 本阶段不做 + +1. 不强制对外输出 provider 模式 +2. 不做“在线全文编辑 ACL4SSR 大规则文件”为主交互方式 +3. 不做复杂多用户权限系统 +4. 不做重型前后端分离 +5. 不把所有规则全文放 SQLite +6. 不引入 Mongo 做主存储 + +--- + +## 4. 架构原则 + +### 原则 1:外部简单,内部灵活 + +客户端只拿一个完整 YAML。 +服务端内部怎么拆、怎么缓存、怎么组合,都由 sub-provider 负责。 + +### 原则 2:规则内容文件化,组合关系数据库化 + +规则正文属于“资产”,继续放文件。 +面板管理的是“配置和组合关系”,放 SQLite。 + +### 原则 3:90% 的配置修改通过“选模块 + 改参数”完成 + +不要把面板做成“大文本框配置编辑器”。 + +### 原则 4:优先可维护,而不是一开始就追求炫技 + +先把结构跑顺,再考虑高级模式。 + +--- + +## 5. 总体架构 + +```text +输入源 + -> 拉取缓存 + -> 原始内容缓存 + -> 解析节点 + -> 节点标准化 + -> 节点处理(过滤/去重/重命名/分类) + -> 策略组生成 + -> 规则模块加载 + -> 规则模块参数渲染 + -> 规则按顺序拼接 + -> 插入 prepend/append 自定义规则 + -> 渲染完整 YAML + -> 结果缓存 / 生成快照 + -> 提供下载 / 预览 / HEAD 信息 +``` + +--- + +## 6. 推荐技术栈 + +### 后端 + +* FastAPI + +### 数据库 + +* SQLite(默认) +* SQLAlchemy ORM +* Alembic migration + +### 模板 + +* Jinja2 + +### 前端 + +* 优先服务端渲染 +* 可用 Jinja2 + HTMX 或简单模板页 +* 先不要求 React/Vue 前后端分离 + +### 缓存 + +* 轻量场景下可先用: + + * SQLite 表 + * 本地文件缓存 +* 不必一上来引入 Redis + +--- + +## 7. 目录结构建议 + +```text +app/ + api/ + routes_sources.py + routes_profiles.py + routes_rules.py + routes_preview.py + core/ + config.py + database.py + cache.py + db/ + base.py + session.py + models/ + source.py + profile.py + rule_module.py + policy_group.py + artifact.py + schemas/ + source.py + profile.py + rule_module.py + services/ + source_fetcher.py + source_parser.py + proxy_normalizer.py + proxy_processor.py + policy_group_builder.py + rule_loader.py + rule_renderer.py + profile_renderer.py + artifact_service.py + templates/ + base.html + sources.html + profiles.html + rules.html + preview.html + main.py + +rules/ + modules/ + lan.list + private.list + cn_domain.list + cn_ip.list + apple.list + microsoft.list + github.list + telegram.list + openai.list + streaming.list + ads.list + final_proxy.list + final_direct.list + + presets/ + minimal.yaml + daily.yaml + router.yaml + + group_templates/ + basic.yaml + ai.yaml + streaming.yaml + +data/ + app.db + cache/ + raw_sources/ + parsed_sources/ + generated/ + artifacts/ + +migrations/ +tests/ +``` + +--- + +## 8. 规则系统设计 + +## 8.1 规则正文:继续放文件 + +规则正文文件继续按 ACL4SSR 风格维护。 + +例如: + +* `rules/modules/apple.list` +* `rules/modules/openai.list` +* `rules/modules/telegram.list` +* `rules/modules/cn_domain.list` +* `rules/modules/final_proxy.list` + +### 推荐格式 + +规则文件使用模板变量,不直接写死策略组: + +```text +DOMAIN-SUFFIX,openai.com,{{ target_group }} +DOMAIN-SUFFIX,chatgpt.com,{{ target_group }} +DOMAIN-SUFFIX,oaistatic.com,{{ target_group }} +DOMAIN-SUFFIX,auth0.openai.com,{{ target_group }} +``` + +这样同一个规则模块可复用到不同策略组。 + +--- + +## 8.2 规则模块:放 SQLite + +SQLite 里存规则模块元数据,而不是规则全文。 + +### 表:`rule_modules` + +字段建议: + +* `id` +* `key` +* `name` +* `description` +* `file_path` +* `category` +* `default_enabled` +* `default_order` +* `default_target_policy` +* `params_schema_json` +* `created_at` +* `updated_at` + +### 示例 + +```json +{ + "key": "openai", + "name": "OpenAI", + "file_path": "rules/modules/openai.list", + "category": "service", + "default_enabled": true, + "default_order": 80, + "default_target_policy": "🤖 AI", + "params_schema_json": { + "target_group": { + "type": "string", + "default": "🤖 AI" + } + } +} +``` + +--- + +## 8.3 Profile 中的规则组合:放 SQLite + +### 表:`profile_rule_modules` + +字段建议: + +* `id` +* `profile_id` +* `module_id` +* `enabled` +* `order_index` +* `target_policy` +* `params_json` + +含义: + +* 这个 profile 是否启用这个模块 +* 顺序是什么 +* 最终绑定到哪个策略组 +* 参数是什么 + +--- + +## 8.4 规则预设 + +可选支持“预设”。 + +例如: + +* `minimal` +* `daily` +* `router` + +但预设只作为**初始化模板**,最终仍落到具体 profile 配置里。 + +不要求上线初期就把预设做得很复杂。 + +--- + +## 9. 策略组系统设计 + +策略组不要硬编码死在一个大 YAML 模板里。 +也要做成可配置对象。 + +### 表:`policy_groups` + +字段建议: + +* `id` +* `profile_id` +* `name` +* `type` +* `order_index` +* `config_json` + +### `config_json` 示例 + +```json +{ + "proxies": ["DIRECT", "REJECT", "香港节点", "日本节点"], + "include_all_nodes": false, + "filter": "HK|Hong Kong" +} +``` + +### 支持的策略组类型 + +* `select` +* `url-test` +* `fallback` +* `load-balance` + +### 建议的默认组 + +* `🚀 节点选择` +* `🤖 AI` +* `📺 流媒体` +* `🍎 苹果服务` +* `📲 Telegram` +* `🌍 国外网站` +* `🇨🇳 国内网站` + +--- + +## 10. 源管理设计 + +### 表:`sources` + +字段建议: + +* `id` +* `name` +* `type` + 值可为: + + * `base64` + * `url` + * `link` + * `static` +* `content` +* `headers_json` +* `enabled` +* `priority` +* `update_interval_sec` +* `rename_rules_json` +* `filter_rules_json` +* `dedupe_policy` +* `last_sync_at` +* `last_sync_status` +* `last_error` +* `created_at` +* `updated_at` + +### 说明 + +这里的源是原始输入源,不是给客户端的 provider。 + +--- + +## 11. Profile 设计 + +Profile 是最终对外输出的配置单元。 + +例如: + +* iPhone 配置 +* Windows 配置 +* OpenWrt 配置 +* Apple TV 配置 +* 精简版配置 +* 完整版配置 + +### 表:`profiles` + +字段建议: + +* `id` +* `name` +* `description` +* `source_ids_json` +* `dns_template` +* `tun_enabled` +* `udp_enabled` +* `append_subscription_info` +* `expose_head_info` +* `enabled` +* `created_at` +* `updated_at` + +--- + +## 12. 自定义覆盖设计 + +为了兼容高级用户需求,支持少量覆盖,但不做全文编辑器。 + +### 表:`profile_overrides` + +字段建议: + +* `id` +* `profile_id` +* `custom_rules_prepend` +* `custom_rules_append` +* `custom_proxy_groups` +* `custom_dns_patch` +* `custom_yaml_patch` + +### 原则 + +只提供: + +* prepend rules +* append rules +* 少量 patch + +不鼓励直接在面板里手写全部 YAML。 + +--- + +## 13. 缓存设计 + +## 13.1 原始源缓存 + +缓存拉回来的原始内容。 + +目的: + +* 避免频繁请求订阅源 +* 降低上游压力 +* 出问题可复现 + +--- + +## 13.2 解析结果缓存 + +把原始内容解析为统一节点模型后缓存。 + +目的: + +* 多 profile 复用解析结果 +* 避免重复解析 + +--- + +## 13.3 最终 YAML 缓存 + +同一个 profile 在以下内容不变时,直接返回缓存: + +* 订阅源内容未变 +* profile 配置未变 +* 规则模块未变 +* 策略组未变 + +缓存键建议基于 hash: + +* 源快照 hash +* 规则快照 hash +* profile 配置 hash + +--- + +## 14. 生成快照设计 + +### 表:`generated_artifacts` + +字段建议: + +* `id` +* `profile_id` +* `version` +* `content` +* `content_hash` +* `source_snapshot_json` +* `rule_snapshot_json` +* `generated_at` + +### 用途 + +* 预览 +* diff +* 回滚 +* 调试 +* 追踪“为什么这次生成结果变了” + +--- + +## 15. 内部统一节点模型 + +建议在解析层统一成一个内部对象。 + +### Python 草案 + +```python +from pydantic import BaseModel +from typing import Any + +class ProxyNode(BaseModel): + name: str + type: str + server: str + port: int + udp: bool = True + tags: list[str] = [] + raw: dict[str, Any] = {} +``` + +对不同类型协议(ss、vmess、vless、trojan、hysteria 等)额外字段可放在 `raw` 里。 + +--- + +## 16. 生成流水线 + +## 第 1 步:拉取源 + +* 读取启用的 sources +* 获取原始订阅内容 +* 更新缓存 +* 记录同步状态 + +## 第 2 步:解析源 + +* 解析 base64 +* 解析单链接 +* 解析远程订阅 +* 标准化成统一节点模型 + +## 第 3 步:节点处理 + +* 去重 +* 重命名 +* 按国家/协议/标签分类 +* 过滤无效节点 +* 应用 profile 的筛选逻辑 + +## 第 4 步:生成策略组 + +* 读取 profile 绑定的 policy groups +* 根据节点分类与过滤规则生成 `proxy-groups` + +## 第 5 步:加载规则模块 + +* 读取 profile 启用的规则模块 +* 按顺序加载规则文件 +* 渲染模板变量(如 `target_group`) + +## 第 6 步:组装规则 + +* 拼接所有规则模块 +* 插入 prepend rules +* 插入 append rules +* 保证 `MATCH` 规则最后输出 + +## 第 7 步:生成完整 YAML + +输出包含: + +* mixed-port / socks-port / redir-port / tproxy-port(按需) +* mode +* log-level +* dns +* tun +* proxies +* proxy-groups +* rules + +## 第 8 步:缓存与快照 + +* 计算 hash +* 命中缓存则复用 +* 否则生成新 artifact + +--- + +## 17. 输出设计 + +### 对外输出接口 + +* `/profiles/{id}/clash.yaml` +* `/profiles/{id}/download` +* `/profiles/{id}/preview` +* `/profiles/{id}/head` + +### 输出原则 + +* 默认返回完整 YAML +* 对客户端透明 +* 客户端无需理解内部模块化结构 + +### 可选响应头 + +支持将源站订阅信息透传到最终响应头,例如: + +* `subscription-userinfo` +* 其他流量额度相关头 + +如果多个源混合: + +* 默认只透传第一个主源的额度信息 +* 或后续设计聚合策略,但不是当前必做 + +--- + +## 18. API 草案 + +## 源管理 + +* `GET /api/sources` +* `POST /api/sources` +* `PUT /api/sources/{id}` +* `DELETE /api/sources/{id}` +* `POST /api/sources/{id}/test` +* `POST /api/sources/{id}/sync` + +## 规则模块 + +* `GET /api/rule-modules` +* `GET /api/rule-modules/{id}` +* `PUT /api/rule-modules/{id}` + +## Profile + +* `GET /api/profiles` +* `POST /api/profiles` +* `PUT /api/profiles/{id}` +* `DELETE /api/profiles/{id}` + +## Profile 规则配置 + +* `GET /api/profiles/{id}/rules` +* `PUT /api/profiles/{id}/rules` + +## Profile 策略组配置 + +* `GET /api/profiles/{id}/policy-groups` +* `PUT /api/profiles/{id}/policy-groups` + +## 预览与生成 + +* `POST /api/profiles/{id}/preview` +* `POST /api/profiles/{id}/generate` +* `GET /api/profiles/{id}/artifacts` +* `GET /api/artifacts/{id}` + +--- + +## 19. 面板设计原则 + +### 普通模式 + +允许: + +* 配源 +* 选 profile +* 勾选规则模块 +* 选择规则模块绑定的策略组 +* 调模块顺序 +* 加 prepend / append 规则 +* 预览生成结果 + +### 高级模式 + +允许: + +* 编辑少量覆盖配置 +* 自定义组模板 +* 导入自定义规则模块 + +### 不建议 + +* 直接在线编辑整份 ACL4SSR 规则大文件作为主要操作方式 + +--- + +## 20. 示例规则模块 + +## `rules/modules/openai.list` + +```text +DOMAIN-SUFFIX,openai.com,{{ target_group }} +DOMAIN-SUFFIX,chatgpt.com,{{ target_group }} +DOMAIN-SUFFIX,oaistatic.com,{{ target_group }} +DOMAIN-SUFFIX,auth0.openai.com,{{ target_group }} +``` + +## `rules/modules/apple.list` + +```text +DOMAIN-SUFFIX,apple.com,{{ target_group }} +DOMAIN-SUFFIX,icloud.com,{{ target_group }} +DOMAIN-SUFFIX,itunes.apple.com,{{ target_group }} +DOMAIN-SUFFIX,apps.apple.com,{{ target_group }} +``` + +--- + +## 21. 示例最终输出 YAML 骨架 + +```yaml +mixed-port: 7890 +allow-lan: true +mode: rule +log-level: info + +dns: + enable: true + ipv6: false + nameserver: + - 223.5.5.5 + - 119.29.29.29 + +proxies: + - name: 香港节点 01 + type: ss + server: 1.2.3.4 + port: 443 + cipher: aes-128-gcm + password: xxxx + +proxy-groups: + - name: 🚀 节点选择 + type: select + proxies: + - 香港节点 01 + - 日本节点 01 + - DIRECT + + - name: 🤖 AI + type: select + proxies: + - 🚀 节点选择 + - 香港节点 01 + - 日本节点 01 + +rules: + - DOMAIN-SUFFIX,openai.com,🤖 AI + - DOMAIN-SUFFIX,chatgpt.com,🤖 AI + - DOMAIN-SUFFIX,apple.com,🍎 苹果服务 + - GEOIP,CN,DIRECT + - MATCH,🚀 节点选择 +``` + +--- + +## 22. 数据库配置要求 + +### 默认 + +```env +DATABASE_URL=sqlite:///./data/app.db +``` + +### 未来切 PostgreSQL + +```env +DATABASE_URL=postgresql+psycopg://user:pass@postgres:5432/subprovider +``` + +### 要求 + +* 所有数据库访问都走 SQLAlchemy +* migration 走 Alembic +* 不写死 SQLite 特性到业务逻辑中 + +--- + +## 23. 开发阶段建议 + +## 第一阶段:解耦与最小可用 + +目标: + +* 先跑通结构 + +任务: + +1. 引入 SQLAlchemy + Alembic +2. 建 sources / profiles / rule_modules / profile_rule_modules / policy_groups / artifacts 表 +3. 将现有规则整理为 `rules/modules/*.list` +4. 将现有单体生成逻辑拆成 service +5. 支持生成一个完整 YAML +6. 提供最简预览页面 + +--- + +## 第二阶段:面板化 + +目标: + +* 基础可用 + +任务: + +1. 源管理页面 +2. Profile 管理页面 +3. 规则模块勾选和排序页面 +4. 策略组配置页面 +5. 预览和下载页面 + +--- + +## 第三阶段:增强能力 + +目标: + +* 更稳定、更可追踪 + +任务: + +1. 快照与 diff +2. 生成缓存优化 +3. 同步日志和错误展示 +4. 自定义规则导入 +5. 自定义组模板 + +--- + +## 24. 明确不推荐的方向 + +1. 不推荐把 ACL4SSR 规则逐条入 SQLite +2. 不推荐面板主交互是“编辑一万条规则文本” +3. 不推荐一开始就做 rule-provider / proxy-provider 对外发布 +4. 不推荐 Mongo 作为主配置库 +5. 不推荐先做复杂权限系统 +6. 不推荐先做重前端再补业务逻辑 + +--- + +## 25. 给 Codex 的明确执行要求 + +请按以下要求重构项目: + +1. 保留对外输出为单个完整 YAML +2. 将内部生成逻辑改为模块化流水线 +3. 规则正文继续使用文件存储 +4. 使用 SQLite 存储: + + * 源配置 + * profile 配置 + * 规则模块元数据 + * profile 与规则模块的绑定关系 + * 策略组配置 + * 生成快照 +5. 使用 SQLAlchemy + Alembic,数据库 URL 可配置 +6. 代码结构按 service 分层,避免单文件堆逻辑 +7. 首先实现最小可运行版本: + + * 能配置 source + * 能配置 profile + * 能选择规则模块 + * 能选择目标策略组 + * 能生成并预览完整 YAML +8. 不要实现对外 provider 模式作为默认路线 +9. 不要将规则正文全文入库 +10. 不要将面板设计成规则全文编辑器 + +--- + +## 26. 最终一句话总结 + +本项目的最终路线是: + +> **内部模块化、缓存化、可配置化** +> **对外输出完整单文件 YAML** +> **规则正文继续文件维护** +> **规则组合关系和面板配置放 SQLite** + +这条路线最符合当前项目阶段,也最稳。 + +--- + +如果你要,我下一条可以继续给你一份**更像任务单的 Codex 执行拆分版**,我会按“先改哪些文件、再改哪些文件、每步验收什么”来写。