feat: direct subscription fetching, URI parser, FLClashX compatibility
- Fetch subscriptions directly via httpx (mihomo UA → YAML, xray-checker → base64 URI list) - Add uri_parser.py: vless/vmess/ss/trojan/hysteria2 URI → Mihomo proxy dicts - Fix YAML quoting for Go parser (strings starting with special chars) - Remove hidden:true and proxy-providers from delivered configs - Inject all service groups into GLOBAL proxies for FLClashX group discovery - Strip placeholder proxy-providers (e.g. "subscription") not in DB - Fix Mihomo healthcheck: add Bearer auth header - Add README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+56
-6
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
import os
|
||||
import yaml
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import httpx
|
||||
import uuid
|
||||
from fastapi import FastAPI, Depends, HTTPException, Form, Request
|
||||
from fastapi.responses import Response, RedirectResponse
|
||||
@@ -13,7 +15,7 @@ from sqlalchemy import select, desc, make_url
|
||||
|
||||
from models import Base, Config, Subscription, ExportLog, make_engine, make_session_factory
|
||||
from mihomo import MihomoClient
|
||||
from expander import expand_config, build_mihomo_config
|
||||
from expander import expand_config, build_mihomo_config, inject_providers_for_delivery
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@@ -42,7 +44,29 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
async def write_and_reload_mihomo(db: AsyncSession) -> None:
|
||||
result = await db.execute(select(Config))
|
||||
configs = result.scalars().all()
|
||||
config_yaml = build_mihomo_config([c.base_yaml for c in configs], MIHOMO_SECRET)
|
||||
mihomo_yaml = build_mihomo_config([c.base_yaml for c in configs], MIHOMO_SECRET)
|
||||
mihomo_cfg = yaml.safe_load(mihomo_yaml) or {}
|
||||
existing = set((mihomo_cfg.get("proxy-providers") or {}).keys())
|
||||
sub_result = await db.execute(select(Subscription))
|
||||
all_subs = sub_result.scalars().all()
|
||||
providers = mihomo_cfg.setdefault("proxy-providers", {}) if existing or all_subs else {}
|
||||
for sub in all_subs:
|
||||
if sub.name not in existing:
|
||||
providers[sub.name] = {
|
||||
"type": "http",
|
||||
"url": sub.url,
|
||||
"interval": 3600,
|
||||
"health-check": {
|
||||
"enable": True,
|
||||
"url": "https://www.gstatic.com/generate_204",
|
||||
"interval": 300,
|
||||
},
|
||||
}
|
||||
if providers:
|
||||
mihomo_cfg["proxy-providers"] = providers
|
||||
else:
|
||||
mihomo_cfg.pop("proxy-providers", None)
|
||||
config_yaml = yaml.dump(mihomo_cfg, allow_unicode=True, sort_keys=False)
|
||||
config_path = os.path.join(MIHOMO_CONFIG_DIR, "config.yaml")
|
||||
tmp_path = config_path + ".tmp"
|
||||
os.makedirs(MIHOMO_CONFIG_DIR, exist_ok=True)
|
||||
@@ -76,6 +100,30 @@ async def lifespan(app: FastAPI): # type: ignore[type-arg]
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
async def _fetch_subscription(url: str) -> list[dict]:
|
||||
"""Fetch subscription. Tries mihomo UA first (YAML), falls back to xray-checker (base64 URI list)."""
|
||||
import base64
|
||||
from uri_parser import parse_proxy_uris
|
||||
|
||||
async with httpx.AsyncClient(follow_redirects=True) as c:
|
||||
try:
|
||||
resp = await c.get(url, timeout=30.0, headers={"User-Agent": "mihomo"})
|
||||
resp.raise_for_status()
|
||||
cfg = yaml.safe_load(resp.text) or {}
|
||||
proxies = cfg.get("proxies") or []
|
||||
if proxies:
|
||||
return proxies
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
resp = await c.get(url, timeout=30.0, headers={"User-Agent": "xray-checker"})
|
||||
resp.raise_for_status()
|
||||
raw = resp.content.strip()
|
||||
raw += b"=" * (-len(raw) % 4)
|
||||
decoded = base64.b64decode(raw).decode("utf-8", errors="replace")
|
||||
return parse_proxy_uris(decoded)
|
||||
|
||||
|
||||
@app.get("/config/{token}.yaml")
|
||||
async def get_config(
|
||||
token: str,
|
||||
@@ -96,15 +144,15 @@ async def get_config(
|
||||
|
||||
for sub in subscriptions:
|
||||
try:
|
||||
proxies = await mihomo_client.refresh_and_collect(sub.name, timeout=30)
|
||||
provider_proxies[sub.name] = proxies
|
||||
provider_proxies[sub.name] = await _fetch_subscription(sub.url)
|
||||
sub.last_fetched_at = datetime.utcnow()
|
||||
except Exception as exc:
|
||||
logger.error("Failed to refresh provider %s: %s", sub.name, exc)
|
||||
logger.error("Failed to fetch subscription %s: %s", sub.name, exc)
|
||||
errors.append(f"{sub.name}: {exc}")
|
||||
|
||||
try:
|
||||
expanded = expand_config(config.base_yaml, provider_proxies)
|
||||
injected_yaml = inject_providers_for_delivery(config.base_yaml, subscriptions)
|
||||
expanded = expand_config(injected_yaml, provider_proxies)
|
||||
except Exception as exc:
|
||||
logger.error("Config expansion failed for token (redacted): %s", exc, exc_info=True)
|
||||
db.add(
|
||||
@@ -239,6 +287,7 @@ async def create_subscription(
|
||||
sub = Subscription(config_id=config_id, name=name, url=url)
|
||||
db.add(sub)
|
||||
await db.commit()
|
||||
await write_and_reload_mihomo(db)
|
||||
return RedirectResponse(f"/configs/{config_id}", status_code=303)
|
||||
|
||||
|
||||
@@ -250,6 +299,7 @@ async def delete_subscription(sub_id: int, db: AsyncSession = Depends(get_db)):
|
||||
config_id = sub.config_id
|
||||
await db.delete(sub)
|
||||
await db.commit()
|
||||
await write_and_reload_mihomo(db)
|
||||
return RedirectResponse(f"/configs/{config_id}", status_code=303)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user