211 lines
6.5 KiB
Python
211 lines
6.5 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import time
|
||
from typing import Any, Awaitable, Callable, Optional
|
||
|
||
from fastapi import APIRouter, Query
|
||
from fastapi.responses import JSONResponse
|
||
|
||
from web2_ip_profile import run_ip_profile
|
||
from web2_ip_quality import run_ip_quality
|
||
from web2_network import run_network_dashboard, run_network_toolkit
|
||
|
||
router = APIRouter()
|
||
|
||
_PROFILE_TTL_SECONDS = 300.0
|
||
_QUALITY_TTL_SECONDS = 600.0
|
||
_NETWORK_TTL_SECONDS = 300.0
|
||
_NETWORK_TOOLKIT_TTL_SECONDS = 300.0
|
||
|
||
_cache_lock = asyncio.Lock()
|
||
_profile_cache: dict[str, tuple[float, dict[str, Any]]] = {}
|
||
_quality_cache: tuple[float, dict[str, Any]] | None = None
|
||
_network_cache: tuple[float, dict[str, Any]] | None = None
|
||
_network_toolkit_cache: tuple[float, dict[str, Any]] | None = None
|
||
_inflight: dict[str, asyncio.Task[dict[str, Any]]] = {}
|
||
|
||
|
||
def _profile_cache_key(tz: str, lang: str, leak_flag: Optional[bool]) -> str:
|
||
leak = "none" if leak_flag is None else ("1" if leak_flag else "0")
|
||
return f"{tz}|{lang}|{leak}"
|
||
|
||
|
||
async def _cached_fetch(
|
||
key: str,
|
||
*,
|
||
ttl: float,
|
||
force_refresh: bool,
|
||
fetcher: Callable[[], Awaitable[dict[str, Any]]],
|
||
store: Callable[[str, float, dict[str, Any]], None],
|
||
load: Callable[[str], Optional[tuple[float, dict[str, Any]]]],
|
||
) -> dict[str, Any]:
|
||
now = time.time()
|
||
if not force_refresh:
|
||
cached = load(key)
|
||
if cached and (now - cached[0]) < ttl:
|
||
return cached[1]
|
||
|
||
async with _cache_lock:
|
||
if not force_refresh:
|
||
cached = load(key)
|
||
if cached and (time.time() - cached[0]) < ttl:
|
||
return cached[1]
|
||
inflight = _inflight.get(key)
|
||
if inflight is not None:
|
||
return await asyncio.shield(inflight)
|
||
|
||
task = asyncio.create_task(fetcher())
|
||
_inflight[key] = task
|
||
|
||
try:
|
||
payload = await task
|
||
finally:
|
||
async with _cache_lock:
|
||
if _inflight.get(key) is task:
|
||
_inflight.pop(key, None)
|
||
|
||
store(key, time.time(), payload)
|
||
return payload
|
||
|
||
|
||
def _store_profile(key: str, at: float, payload: dict[str, Any]) -> None:
|
||
_profile_cache[key] = (at, payload)
|
||
|
||
|
||
def _load_profile(key: str) -> Optional[tuple[float, dict[str, Any]]]:
|
||
return _profile_cache.get(key)
|
||
|
||
|
||
def _store_quality(key: str, at: float, payload: dict[str, Any]) -> None:
|
||
global _quality_cache
|
||
_quality_cache = (at, payload)
|
||
|
||
|
||
def _load_quality(_key: str) -> Optional[tuple[float, dict[str, Any]]]:
|
||
return _quality_cache
|
||
|
||
|
||
def _store_network(_key: str, at: float, payload: dict[str, Any]) -> None:
|
||
global _network_cache
|
||
_network_cache = (at, payload)
|
||
|
||
|
||
def _load_network(_key: str) -> Optional[tuple[float, dict[str, Any]]]:
|
||
return _network_cache
|
||
|
||
|
||
def _store_network_toolkit(_key: str, at: float, payload: dict[str, Any]) -> None:
|
||
global _network_toolkit_cache
|
||
_network_toolkit_cache = (at, payload)
|
||
|
||
|
||
def _load_network_toolkit(_key: str) -> Optional[tuple[float, dict[str, Any]]]:
|
||
return _network_toolkit_cache
|
||
|
||
|
||
async def _get_ip_profile_cached(
|
||
*,
|
||
tz: str,
|
||
lang: str,
|
||
leak_flag: Optional[bool],
|
||
force_refresh: bool,
|
||
) -> dict[str, Any]:
|
||
cache_key = _profile_cache_key(tz, lang, leak_flag)
|
||
return await _cached_fetch(
|
||
cache_key,
|
||
ttl=_PROFILE_TTL_SECONDS,
|
||
force_refresh=force_refresh,
|
||
fetcher=lambda: run_ip_profile(client_tz=tz, client_lang=lang, webrtc_leak=leak_flag),
|
||
store=_store_profile,
|
||
load=_load_profile,
|
||
)
|
||
|
||
|
||
async def _get_ip_quality_cached(*, force_refresh: bool) -> dict[str, Any]:
|
||
return await _cached_fetch(
|
||
"quality",
|
||
ttl=_QUALITY_TTL_SECONDS,
|
||
force_refresh=force_refresh,
|
||
fetcher=run_ip_quality,
|
||
store=_store_quality,
|
||
load=_load_quality,
|
||
)
|
||
|
||
|
||
async def _get_network_status_cached(*, force_refresh: bool) -> dict[str, Any]:
|
||
return await _cached_fetch(
|
||
"network_v2",
|
||
ttl=_NETWORK_TTL_SECONDS,
|
||
force_refresh=force_refresh,
|
||
fetcher=run_network_dashboard,
|
||
store=_store_network,
|
||
load=_load_network,
|
||
)
|
||
|
||
|
||
async def _get_network_toolkit_cached(*, force_refresh: bool) -> dict[str, Any]:
|
||
return await _cached_fetch(
|
||
"network_toolkit_v2",
|
||
ttl=_NETWORK_TOOLKIT_TTL_SECONDS,
|
||
force_refresh=force_refresh,
|
||
fetcher=run_network_toolkit,
|
||
store=_store_network_toolkit,
|
||
load=_load_network_toolkit,
|
||
)
|
||
|
||
|
||
@router.get("/network_status")
|
||
async def network_status(
|
||
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
|
||
):
|
||
"""Google / 抖音 连通与简易网速(供仪表盘)。"""
|
||
try:
|
||
return await _get_network_status_cached(force_refresh=force_refresh)
|
||
except Exception as e:
|
||
return JSONResponse({"error": str(e)}, status_code=500)
|
||
|
||
|
||
@router.get("/network_toolkit")
|
||
async def network_toolkit(
|
||
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
|
||
):
|
||
"""网络增强工具聚合:speedtest、mtr、ipinfo、system 信息等。"""
|
||
try:
|
||
return await _get_network_toolkit_cached(force_refresh=force_refresh)
|
||
except Exception as e:
|
||
return JSONResponse({"error": str(e)}, status_code=500)
|
||
|
||
|
||
@router.get("/ip_profile")
|
||
async def ip_profile(
|
||
tz: str = Query("", description="客户端 IANA 时区,如 Asia/Shanghai"),
|
||
lang: str = Query("", description="客户端语言,如 zh-CN"),
|
||
webrtc_leak: Optional[str] = Query(None, description="1/true 表示检测到 WebRTC 本地泄露"),
|
||
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
|
||
):
|
||
"""静态住宅 IP / 出口画像(参考 whoer、ping0.cc)。"""
|
||
leak_flag: Optional[bool] = None
|
||
if webrtc_leak is not None:
|
||
leak_flag = str(webrtc_leak).strip().lower() in ("1", "true", "yes")
|
||
try:
|
||
return await _get_ip_profile_cached(
|
||
tz=tz.strip(),
|
||
lang=lang.strip(),
|
||
leak_flag=leak_flag,
|
||
force_refresh=force_refresh,
|
||
)
|
||
except Exception as e:
|
||
return JSONResponse({"error": str(e)}, status_code=500)
|
||
|
||
|
||
@router.get("/ip_quality")
|
||
async def ip_quality(
|
||
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
|
||
):
|
||
"""IP 质量检测(集成 xykt/IPQuality 数据源与流媒体解锁)。"""
|
||
try:
|
||
return await _get_ip_quality_cached(force_refresh=force_refresh)
|
||
except Exception as e:
|
||
return JSONResponse({"error": str(e)}, status_code=500)
|