This commit is contained in:
riglen
2026-04-09 11:17:06 +08:00
parent 4e97dbc369
commit 5c86136912
4 changed files with 49 additions and 1 deletions

View File

@@ -15,6 +15,9 @@ PUBLIC_BASE_URL=
# 1. Clash/Mihomo YAML proxies 文件
# 2. base64 编码 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_B_URL=

View File

@@ -48,6 +48,7 @@ sub-provider/
cn-ip.yaml
data/
cache/
sources/
.env.example
Dockerfile
docker-compose.yaml
@@ -69,6 +70,7 @@ cp .env.example .env
- `PUBLIC_PATH` 改成足够长的随机字符串
- `PUBLIC_BASE_URL` 建议填写你反代后的最终访问地址,例如 `https://sub.example.com`
- `AIRPORT_A_URL` / `AIRPORT_B_URL` 都可以直接填订阅地址,项目会自动判断是 YAML 还是 URI 订阅
- 也可以直接填本地文件路径,例如 `/app/data/sources/airport-b.txt``file:///app/data/sources/airport-b.txt`
- 允许把其中一个留空;留空时这个机场会自动跳过
3. 启动:
@@ -148,6 +150,7 @@ HEAD /<PUBLIC_PATH>/bundle/stash.yaml?sources=airport-a,airport-b
- 生成后的完整 YAML 会缓存到 `output/bundle-cache/`,默认 600 秒内直接返回缓存
- 可用 `force_refresh=true` 强制跳过缓存并覆盖旧缓存文件
- 上游订阅原始内容也会按 TTL 落盘缓存到 `data/fetch-cache/`,默认 900 秒
- 如果你想固定使用本地文件订阅,可以把文件放到 `data/sources/`,再把 `AIRPORT_*_URL` 指向 `/app/data/sources/xxx.txt`
---

View File

@@ -4,13 +4,14 @@ import base64
import json
import logging
import re
from pathlib import Path
from typing import Any, Iterable
from urllib.parse import parse_qs, unquote, urlparse
import httpx
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.services.cache import TTLCache
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)
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)
disk_cached = load_fetch_cache(
cache_dir=settings.fetch_cache_dir,
@@ -66,6 +78,34 @@ async def fetch_source(name: str, source: SourceConfig) -> FetchResult:
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:
settings = get_settings()
ttl = source.cache_ttl_seconds or settings.cache_ttl_seconds

View File

@@ -15,6 +15,8 @@ sources:
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: "流量|重置|到期|续费|官网|离线|套餐"