B
This commit is contained in:
@@ -15,6 +15,9 @@ PUBLIC_BASE_URL=
|
|||||||
# 1. Clash/Mihomo YAML proxies 文件
|
# 1. Clash/Mihomo YAML proxies 文件
|
||||||
# 2. base64 编码 URI 订阅
|
# 2. base64 编码 URI 订阅
|
||||||
# 3. 直接明文 URI 订阅
|
# 3. 直接明文 URI 订阅
|
||||||
|
# 也支持本地文件,例如:
|
||||||
|
# AIRPORT_B_URL=/app/data/sources/airport-b.txt
|
||||||
|
# AIRPORT_B_URL=file:///app/data/sources/airport-b.txt
|
||||||
AIRPORT_A_URL=
|
AIRPORT_A_URL=
|
||||||
# 允许留空;留空时该机场会自动跳过,不参与默认查询
|
# 允许留空;留空时该机场会自动跳过,不参与默认查询
|
||||||
AIRPORT_B_URL=
|
AIRPORT_B_URL=
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ sub-provider/
|
|||||||
cn-ip.yaml
|
cn-ip.yaml
|
||||||
data/
|
data/
|
||||||
cache/
|
cache/
|
||||||
|
sources/
|
||||||
.env.example
|
.env.example
|
||||||
Dockerfile
|
Dockerfile
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
@@ -69,6 +70,7 @@ cp .env.example .env
|
|||||||
- `PUBLIC_PATH` 改成足够长的随机字符串
|
- `PUBLIC_PATH` 改成足够长的随机字符串
|
||||||
- `PUBLIC_BASE_URL` 建议填写你反代后的最终访问地址,例如 `https://sub.example.com`
|
- `PUBLIC_BASE_URL` 建议填写你反代后的最终访问地址,例如 `https://sub.example.com`
|
||||||
- `AIRPORT_A_URL` / `AIRPORT_B_URL` 都可以直接填订阅地址,项目会自动判断是 YAML 还是 URI 订阅
|
- `AIRPORT_A_URL` / `AIRPORT_B_URL` 都可以直接填订阅地址,项目会自动判断是 YAML 还是 URI 订阅
|
||||||
|
- 也可以直接填本地文件路径,例如 `/app/data/sources/airport-b.txt` 或 `file:///app/data/sources/airport-b.txt`
|
||||||
- 允许把其中一个留空;留空时这个机场会自动跳过
|
- 允许把其中一个留空;留空时这个机场会自动跳过
|
||||||
|
|
||||||
3. 启动:
|
3. 启动:
|
||||||
@@ -148,6 +150,7 @@ HEAD /<PUBLIC_PATH>/bundle/stash.yaml?sources=airport-a,airport-b
|
|||||||
- 生成后的完整 YAML 会缓存到 `output/bundle-cache/`,默认 600 秒内直接返回缓存
|
- 生成后的完整 YAML 会缓存到 `output/bundle-cache/`,默认 600 秒内直接返回缓存
|
||||||
- 可用 `force_refresh=true` 强制跳过缓存并覆盖旧缓存文件
|
- 可用 `force_refresh=true` 强制跳过缓存并覆盖旧缓存文件
|
||||||
- 上游订阅原始内容也会按 TTL 落盘缓存到 `data/fetch-cache/`,默认 900 秒
|
- 上游订阅原始内容也会按 TTL 落盘缓存到 `data/fetch-cache/`,默认 900 秒
|
||||||
|
- 如果你想固定使用本地文件订阅,可以把文件放到 `data/sources/`,再把 `AIRPORT_*_URL` 指向 `/app/data/sources/xxx.txt`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
from urllib.parse import parse_qs, unquote, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import ROOT_DIR, get_settings
|
||||||
from app.models import FetchResult, ProviderDocument, SourceConfig, SourceSnapshot
|
from app.models import FetchResult, ProviderDocument, SourceConfig, SourceSnapshot
|
||||||
from app.services.cache import TTLCache
|
from app.services.cache import TTLCache
|
||||||
from app.services.fetch_cache import build_fetch_cache_key, load_fetch_cache, save_fetch_cache
|
from app.services.fetch_cache import build_fetch_cache_key, load_fetch_cache, save_fetch_cache
|
||||||
@@ -31,6 +32,17 @@ async def fetch_source(name: str, source: SourceConfig) -> FetchResult:
|
|||||||
logger.info("fetch_source memory cache hit: source=%s ttl=%s", name, ttl)
|
logger.info("fetch_source memory cache hit: source=%s ttl=%s", name, ttl)
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
|
local_path = resolve_local_source_path(source.url)
|
||||||
|
if local_path is not None:
|
||||||
|
logger.info("fetch_source local file: source=%s path=%s", name, local_path)
|
||||||
|
try:
|
||||||
|
result = FetchResult(text=local_path.read_text(encoding="utf-8"), headers={})
|
||||||
|
except Exception:
|
||||||
|
logger.exception("fetch_source local file read failed: source=%s path=%s", name, local_path)
|
||||||
|
raise
|
||||||
|
_fetch_cache.set(name, result, ttl)
|
||||||
|
return result
|
||||||
|
|
||||||
cache_key = build_fetch_cache_key(name=name, url=source.url)
|
cache_key = build_fetch_cache_key(name=name, url=source.url)
|
||||||
disk_cached = load_fetch_cache(
|
disk_cached = load_fetch_cache(
|
||||||
cache_dir=settings.fetch_cache_dir,
|
cache_dir=settings.fetch_cache_dir,
|
||||||
@@ -66,6 +78,34 @@ async def fetch_source(name: str, source: SourceConfig) -> FetchResult:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_local_source_path(raw: str) -> Path | None:
|
||||||
|
candidate = raw.strip()
|
||||||
|
if not candidate:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parsed = urlparse(candidate)
|
||||||
|
if parsed.scheme in {"http", "https"}:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if parsed.scheme == "file":
|
||||||
|
path_text = unquote(parsed.path or "")
|
||||||
|
if parsed.netloc:
|
||||||
|
path_text = f"//{parsed.netloc}{path_text}"
|
||||||
|
if re.match(r"^/[A-Za-z]:", path_text):
|
||||||
|
path_text = path_text[1:]
|
||||||
|
path = Path(path_text)
|
||||||
|
else:
|
||||||
|
path = Path(candidate)
|
||||||
|
|
||||||
|
if not path.is_absolute():
|
||||||
|
path = ROOT_DIR / path
|
||||||
|
|
||||||
|
resolved = path.resolve()
|
||||||
|
if not resolved.is_file():
|
||||||
|
raise FileNotFoundError(f"Local source file not found: {resolved}")
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
async def build_provider_document(name: str, source: SourceConfig) -> ProviderDocument:
|
async def build_provider_document(name: str, source: SourceConfig) -> ProviderDocument:
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
ttl = source.cache_ttl_seconds or settings.cache_ttl_seconds
|
ttl = source.cache_ttl_seconds or settings.cache_ttl_seconds
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ sources:
|
|||||||
display_name: B
|
display_name: B
|
||||||
kind: auto
|
kind: auto
|
||||||
url: ${AIRPORT_B_URL}
|
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] "
|
prefix: "[B] "
|
||||||
include_regex: ""
|
include_regex: ""
|
||||||
exclude_regex: "流量|重置|到期|续费|官网|离线|套餐"
|
exclude_regex: "流量|重置|到期|续费|官网|离线|套餐"
|
||||||
|
|||||||
Reference in New Issue
Block a user