优化网络与系统检测为缓存优先,避免页面长时间停留在检测中。

将网络/IP 路由拆分并加入去重缓存,系统检测改为线程执行并支持强制刷新,前端改为先展示缓存快照后按需手动重测,降低阻塞与重复探测开销。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric
2026-07-08 14:38:59 -05:00
parent 1287b72193
commit 0661c33610
7 changed files with 375 additions and 126 deletions

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
import { apiFetch, apiUrl } from './api' import { apiFetch, apiUrl } from './api'
import { saveWorkspaceSnapshot } from './workspace' import { saveWorkspaceSnapshot, loadWorkspaceSnapshot } from './workspace'
import ProxySettings from './ProxySettings' import ProxySettings from './ProxySettings'
import { type FlowStep } from './components/worker/WorkerFlowStrip' import { type FlowStep } from './components/worker/WorkerFlowStrip'
import LivePulse from './components/ui/LivePulse' import LivePulse from './components/ui/LivePulse'
@@ -308,74 +308,44 @@ export default function NetworkDashboard() {
const [data, setData] = useState<NetworkPayload | null>(null) const [data, setData] = useState<NetworkPayload | null>(null)
const [ipProfile, setIpProfile] = useState<IpProfilePayload | null>(null) const [ipProfile, setIpProfile] = useState<IpProfilePayload | null>(null)
const [ipProfileErr, setIpProfileErr] = useState<string | null>(null) const [ipProfileErr, setIpProfileErr] = useState<string | null>(null)
const [ipProfileLoading, setIpProfileLoading] = useState(true) const [ipProfileLoading, setIpProfileLoading] = useState(false)
const [ipQuality, setIpQuality] = useState<IpQualityPayload | null>(null) const [ipQuality, setIpQuality] = useState<IpQualityPayload | null>(null)
const [ipQualityErr, setIpQualityErr] = useState<string | null>(null) const [ipQualityErr, setIpQualityErr] = useState<string | null>(null)
const [ipQualityLoading, setIpQualityLoading] = useState(true) const [ipQualityLoading, setIpQualityLoading] = useState(false)
const [proxyCfg, setProxyCfg] = useState<ProxyCfg>({ enabled: false, http: '', socks: '' }) const [proxyCfg, setProxyCfg] = useState<ProxyCfg>({ enabled: false, http: '', socks: '' })
const [err, setErr] = useState<string | null>(null) const [err, setErr] = useState<string | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(false)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const [hasCachedData, setHasCachedData] = useState(false)
const { leak: webrtcLeak } = useWebRtcLeak(true) const { leak: webrtcLeak } = useWebRtcLeak(true)
const loadIpProfile = useCallback(async () => { const load = useCallback(async (opts?: { silent?: boolean; forceRefresh?: boolean }) => {
setIpProfileErr(null)
setIpProfileLoading(true)
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''
const lang = navigator.language || ''
const q = new URLSearchParams({ tz, lang })
if (webrtcLeak != null) q.set('webrtc_leak', webrtcLeak ? '1' : '0')
const res = await apiFetch(apiUrl(`/ip_profile?${q.toString()}`))
const json = (await res.json()) as IpProfilePayload
if (!res.ok) {
setIpProfileErr(json.error || `IP 画像请求失败 ${res.status}`)
setIpProfile(null)
return
}
setIpProfile(json)
saveWorkspaceSnapshot('ip_profile', json)
} catch (e) {
setIpProfileErr(e instanceof Error ? e.message : String(e))
setIpProfile(null)
} finally {
setIpProfileLoading(false)
}
}, [webrtcLeak])
const loadIpQuality = useCallback(async () => {
setIpQualityErr(null)
setIpQualityLoading(true)
try {
const res = await apiFetch(apiUrl('/ip_quality'))
const json = (await res.json()) as IpQualityPayload
if (!res.ok) {
setIpQualityErr(json.error || `IP 质量请求失败 ${res.status}`)
setIpQuality(null)
return
}
setIpQuality(json)
saveWorkspaceSnapshot('ip_quality', json)
} catch (e) {
setIpQualityErr(e instanceof Error ? e.message : String(e))
setIpQuality(null)
} finally {
setIpQualityLoading(false)
}
}, [])
const load = useCallback(async (opts?: { silent?: boolean }) => {
const silent = opts?.silent ?? false const silent = opts?.silent ?? false
const forceRefresh = opts?.forceRefresh ?? false
if (!forceRefresh) return
setErr(null) setErr(null)
if (!silent) setLoading(true) if (!silent) setLoading(true)
else setRefreshing(true) else setRefreshing(true)
setIpProfileLoading(true)
setIpQualityLoading(true)
try { try {
const [netRes, proxyRes] = await Promise.all([ const q = '?force_refresh=1'
apiFetch(apiUrl('/network_status')), const [netRes, proxyRes, profileRes, qualityRes] = await Promise.all([
apiFetch(apiUrl(`/network_status${q}`)),
apiFetch(apiUrl('/proxy_config')), apiFetch(apiUrl('/proxy_config')),
apiFetch(
apiUrl(
`/ip_profile?${new URLSearchParams({
tz: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
lang: navigator.language || '',
...(webrtcLeak != null ? { webrtc_leak: webrtcLeak ? '1' : '0' } : {}),
force_refresh: '1',
}).toString()}`,
),
),
apiFetch(apiUrl('/ip_quality?force_refresh=1')),
]) ])
void loadIpProfile()
void loadIpQuality()
const json = (await netRes.json()) as NetworkPayload & { error?: string } const json = (await netRes.json()) as NetworkPayload & { error?: string }
if (!netRes.ok) { if (!netRes.ok) {
setErr(json.error || `请求失败 ${netRes.status}`) setErr(json.error || `请求失败 ${netRes.status}`)
@@ -383,7 +353,29 @@ export default function NetworkDashboard() {
} else { } else {
setData(json) setData(json)
saveWorkspaceSnapshot('network_status', json) saveWorkspaceSnapshot('network_status', json)
setHasCachedData(true)
} }
const profileJson = (await profileRes.json()) as IpProfilePayload
if (!profileRes.ok) {
setIpProfileErr(profileJson.error || `IP 画像请求失败 ${profileRes.status}`)
setIpProfile(null)
} else {
setIpProfile(profileJson)
setIpProfileErr(null)
saveWorkspaceSnapshot('ip_profile', profileJson)
}
const qualityJson = (await qualityRes.json()) as IpQualityPayload
if (!qualityRes.ok) {
setIpQualityErr(qualityJson.error || `IP 质量请求失败 ${qualityRes.status}`)
setIpQuality(null)
} else {
setIpQuality(qualityJson)
setIpQualityErr(null)
saveWorkspaceSnapshot('ip_quality', qualityJson)
}
if (proxyRes.ok) { if (proxyRes.ok) {
const proxyJson = (await proxyRes.json()) as ProxyCfg const proxyJson = (await proxyRes.json()) as ProxyCfg
setProxyCfg({ setProxyCfg({
@@ -398,22 +390,44 @@ export default function NetworkDashboard() {
} finally { } finally {
setLoading(false) setLoading(false)
setRefreshing(false) setRefreshing(false)
setIpProfileLoading(false)
setIpQualityLoading(false)
} }
}, [loadIpProfile, loadIpQuality]) }, [webrtcLeak])
useEffect(() => { useEffect(() => {
void load() let cancelled = false
}, [load]) void (async () => {
const [net, profile, quality] = await Promise.all([
useEffect(() => { loadWorkspaceSnapshot<NetworkPayload>('network_status'),
const id = window.setInterval(() => void load({ silent: true }), 45000) loadWorkspaceSnapshot<IpProfilePayload>('ip_profile'),
return () => clearInterval(id) loadWorkspaceSnapshot<IpQualityPayload>('ip_quality'),
}, [load]) ])
if (cancelled) return
useEffect(() => { if (net) {
if (webrtcLeak == null) return setData(net)
void loadIpProfile() setHasCachedData(true)
}, [webrtcLeak, loadIpProfile]) }
if (profile) setIpProfile(profile)
if (quality) setIpQuality(quality)
try {
const proxyRes = await apiFetch(apiUrl('/proxy_config'))
if (!cancelled && proxyRes.ok) {
const proxyJson = (await proxyRes.json()) as ProxyCfg
setProxyCfg({
enabled: !!proxyJson.enabled,
http: proxyJson.http || '',
socks: proxyJson.socks || '',
})
}
} catch {
/* 代理配置读取失败不影响展示缓存结果 */
}
})()
return () => {
cancelled = true
}
}, [])
const view = data ?? DEFAULT_NETWORK const view = data ?? DEFAULT_NETWORK
const status = overallStatus(view, loading) const status = overallStatus(view, loading)
@@ -479,7 +493,15 @@ export default function NetworkDashboard() {
: ['net-routes__pill--no', '境内外出口可能未区分'] : ['net-routes__pill--no', '境内外出口可能未区分']
const statusLabel = const statusLabel =
loading ? '检测中' : status === 'ready' ? '网络就绪' : status === 'warn' ? '需关注' : '出口异常' loading || refreshing
? '检测中'
: !hasCachedData
? '待检测'
: status === 'ready'
? '网络就绪'
: status === 'warn'
? '需关注'
: '出口异常'
const pulseTone = status === 'ready' ? 'live' : status === 'warn' ? 'warn' : 'idle' const pulseTone = status === 'ready' ? 'live' : status === 'warn' ? 'warn' : 'idle'
@@ -517,10 +539,10 @@ export default function NetworkDashboard() {
type="button" type="button"
className="btn btn-primary btn--sm net-summary__btn" className="btn btn-primary btn--sm net-summary__btn"
disabled={loading || refreshing || ipProfileLoading || ipQualityLoading} disabled={loading || refreshing || ipProfileLoading || ipQualityLoading}
onClick={() => void load({ silent: !!data })} onClick={() => void load({ silent: hasCachedData, forceRefresh: true })}
> >
<IconRefresh className={loading || refreshing ? 'is-spinning' : ''} /> <IconRefresh className={loading || refreshing ? 'is-spinning' : ''} />
{loading || refreshing ? '检测中…' : '重新检测'} {loading || refreshing ? '检测中…' : hasCachedData ? '重新检测' : '开始检测'}
</button> </button>
<button type="button" className="btn btn-secondary btn--sm" onClick={openProxySettings}> <button type="button" className="btn btn-secondary btn--sm" onClick={openProxySettings}>
Windows 代理 Windows 代理

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { apiFetch, apiUrl } from './api' import { apiFetch, apiUrl } from './api'
import { saveWorkspaceSnapshot } from './workspace' import { saveWorkspaceSnapshot, loadWorkspaceSnapshot } from './workspace'
import MetricBar, { toneFromPercent } from './components/ui/MetricBar' import MetricBar, { toneFromPercent } from './components/ui/MetricBar'
type Metrics = { type Metrics = {
@@ -81,10 +81,18 @@ function InfoRow({ label, value }: { label: string; value: string }) {
export default function SystemMetrics() { export default function SystemMetrics() {
const [data, setData] = useState<Metrics | null>(null) const [data, setData] = useState<Metrics | null>(null)
const [err, setErr] = useState<string | null>(null) const [err, setErr] = useState<string | null>(null)
const [refreshing, setRefreshing] = useState(false)
const [hasCachedData, setHasCachedData] = useState(false)
const inFlightRef = useRef(false)
const load = useCallback(async () => { const load = useCallback(async (opts?: { forceRefresh?: boolean }) => {
const forceRefresh = opts?.forceRefresh ?? false
if (!forceRefresh) return
if (inFlightRef.current) return
inFlightRef.current = true
setRefreshing(true)
try { try {
const res = await apiFetch(apiUrl('/system_metrics')) const res = await apiFetch(apiUrl('/system_metrics?force_refresh=1'))
const json = (await res.json()) as Metrics const json = (await res.json()) as Metrics
if (!res.ok) { if (!res.ok) {
setErr(json.error || `HTTP ${res.status}`) setErr(json.error || `HTTP ${res.status}`)
@@ -92,17 +100,30 @@ export default function SystemMetrics() {
} }
setErr(null) setErr(null)
setData(json) setData(json)
setHasCachedData(true)
saveWorkspaceSnapshot('system_metrics', json) saveWorkspaceSnapshot('system_metrics', json)
} catch (e) { } catch (e) {
setErr(e instanceof Error ? e.message : String(e)) setErr(e instanceof Error ? e.message : String(e))
} finally {
setRefreshing(false)
inFlightRef.current = false
} }
}, []) }, [])
useEffect(() => { useEffect(() => {
void load() let cancelled = false
const id = window.setInterval(() => void load(), 3000) void (async () => {
return () => clearInterval(id) const cached = await loadWorkspaceSnapshot<Metrics>('system_metrics')
}, [load]) if (cancelled) return
if (cached) {
setData(cached)
setHasCachedData(true)
}
})()
return () => {
cancelled = true
}
}, [])
const sys = data?.system const sys = data?.system
const hw = data?.hardware const hw = data?.hardware
@@ -122,7 +143,17 @@ export default function SystemMetrics() {
</h2> </h2>
<p className="card__desc">本机操作系统、处理器、显卡与资源占用</p> <p className="card__desc">本机操作系统、处理器、显卡与资源占用</p>
</div> </div>
<span className="metrics-uptime">⏱ 运行 {fmtUptime(sys?.boot_time_unix)}</span> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="metrics-uptime">⏱ 运行 {fmtUptime(sys?.boot_time_unix)}</span>
<button
type="button"
className="btn btn-secondary btn--sm"
disabled={refreshing}
onClick={() => void load({ forceRefresh: true })}
>
{refreshing ? '检测中…' : hasCachedData ? '重新检测' : '开始检测'}
</button>
</div>
</div> </div>
{err ? ( {err ? (
@@ -209,7 +240,7 @@ export default function SystemMetrics() {
<div className="metrics-disks__title">磁盘空间</div> <div className="metrics-disks__title">磁盘空间</div>
<ul className="metrics-disk-list"> <ul className="metrics-disk-list">
{data.disks.slice(0, 8).map((d) => ( {data.disks.slice(0, 8).map((d) => (
<li key={d.mountpoint}> <li key={`${d.mountpoint || 'unknown'}-${d.device || 'dev'}`}>
<span className="metrics-disk-list__mp"> <span className="metrics-disk-list__mp">
{d.mountpoint} {d.mountpoint}
{d.fstype ? ` (${d.fstype})` : ''} {d.fstype ? ` (${d.fstype})` : ''}

View File

@@ -25,6 +25,18 @@ export function saveWorkspaceSnapshot(key: string, data: unknown): void {
void getWorkspace()?.saveSnapshot(key, data) void getWorkspace()?.saveSnapshot(key, data)
} }
export async function loadWorkspaceSnapshot<T>(key: string): Promise<T | null> {
const ws = getWorkspace()
if (!ws) return null
try {
const dash = await ws.getDashboard()
const item = dash.snapshots[key]
return item?.data != null ? (item.data as T) : null
} catch {
return null
}
}
export function logUserAction(action: string, nav?: string, detail?: unknown): void { export function logUserAction(action: string, nav?: string, detail?: unknown): void {
void getWorkspace()?.logUserAction({ action, nav, detail }) void getWorkspace()?.logUserAction({ action, nav, detail })
} }

61
web2.py
View File

@@ -19,9 +19,7 @@ from web2_supervisor import (
strip_ansi_codes, strip_ansi_codes,
use_builtin, use_builtin,
) )
from web2_network import run_network_dashboard from web2_network_routes import router as network_router
from web2_ip_profile import run_ip_profile
from web2_ip_quality import run_ip_quality
from web2_proxy_config import read_proxy_config, write_proxy_config from web2_proxy_config import read_proxy_config, write_proxy_config
from web2_system_metrics import snapshot as system_metrics_snapshot from web2_system_metrics import snapshot as system_metrics_snapshot
from web2_youtube_oauth import ( from web2_youtube_oauth import (
@@ -100,6 +98,9 @@ CONFIG_DIR = BASE_DIR / "config"
DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs" DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs"
RELAY_DB = relay_db_path(BASE_DIR) RELAY_DB = relay_db_path(BASE_DIR)
_PROCESS_STATUS_CACHE: dict[str, str] = {} _PROCESS_STATUS_CACHE: dict[str, str] = {}
_SYSTEM_METRICS_TTL_SECONDS = 10.0
_SYSTEM_METRICS_CACHE: tuple[float, dict] | None = None
_SYSTEM_METRICS_CACHE_LOCK = asyncio.Lock()
MAX_LOG_LINES = 300 MAX_LOG_LINES = 300
@@ -302,6 +303,7 @@ app.add_middleware(
allow_methods=["GET", "POST", "OPTIONS"], allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"], allow_headers=["*"],
) )
app.include_router(network_router)
@app.get("/client/overview") @app.get("/client/overview")
async def client_overview(): async def client_overview():
@@ -1095,40 +1097,6 @@ async def health():
return {"ok": True} return {"ok": True}
@app.get("/network_status")
async def network_status():
"""Google / 抖音 连通与简易网速(供仪表盘)。"""
try:
return await run_network_dashboard()
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
@app.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 本地泄露"),
):
"""静态住宅 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 run_ip_profile(client_tz=tz.strip(), client_lang=lang.strip(), webrtc_leak=leak_flag)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/ip_quality")
async def ip_quality():
"""IP 质量检测(集成 xykt/IPQuality 数据源与流媒体解锁)。"""
try:
return await run_ip_quality()
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/proxy_config") @app.get("/proxy_config")
async def get_proxy_config(): async def get_proxy_config():
"""仅作用于录制子进程的代理(不写 Windows 系统代理)。""" """仅作用于录制子进程的代理(不写 Windows 系统代理)。"""
@@ -1154,10 +1122,25 @@ async def post_proxy_config(request: Request):
@app.get("/system_metrics") @app.get("/system_metrics")
async def system_metrics(): async def system_metrics(
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
):
"""CPU/内存/磁盘/网卡速率与 ffmpeg 进程摘要。""" """CPU/内存/磁盘/网卡速率与 ffmpeg 进程摘要。"""
global _SYSTEM_METRICS_CACHE
try: try:
return system_metrics_snapshot() now = time.time()
async with _SYSTEM_METRICS_CACHE_LOCK:
if (
not force_refresh
and _SYSTEM_METRICS_CACHE
and (now - _SYSTEM_METRICS_CACHE[0]) < _SYSTEM_METRICS_TTL_SECONDS
):
return _SYSTEM_METRICS_CACHE[1]
payload = await asyncio.to_thread(system_metrics_snapshot)
async with _SYSTEM_METRICS_CACHE_LOCK:
_SYSTEM_METRICS_CACHE = (time.time(), payload)
return payload
except Exception as e: except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500) return JSONResponse({"error": str(e)}, status_code=500)

View File

@@ -4,6 +4,8 @@ from __future__ import annotations
import platform import platform
import socket import socket
import subprocess import subprocess
import threading
import time
from typing import Any from typing import Any
@@ -20,8 +22,10 @@ def detect_nvidia_gpu() -> bool:
return False return False
def detect_nvenc_available() -> bool: def detect_nvenc_available(nvidia_present: bool | None = None) -> bool:
if not detect_nvidia_gpu(): if nvidia_present is None:
nvidia_present = detect_nvidia_gpu()
if not nvidia_present:
return False return False
try: try:
_kw = {"capture_output": True, "text": True, "timeout": 8} _kw = {"capture_output": True, "text": True, "timeout": 8}
@@ -33,13 +37,27 @@ def detect_nvenc_available() -> bool:
return False return False
def gpu_status_payload() -> dict[str, Any]: _GPU_STATUS_CACHE_TTL_SECONDS = 60.0
_gpu_status_lock = threading.Lock()
_gpu_status_cache: tuple[float, dict[str, Any]] | None = None
def gpu_status_payload(force_refresh: bool = False) -> dict[str, Any]:
global _gpu_status_cache
now = time.time()
with _gpu_status_lock:
if not force_refresh and _gpu_status_cache and (now - _gpu_status_cache[0]) < _GPU_STATUS_CACHE_TTL_SECONDS:
return dict(_gpu_status_cache[1])
nv = detect_nvidia_gpu() nv = detect_nvidia_gpu()
enc = detect_nvenc_available() enc = detect_nvenc_available(nv)
return { payload = {
"nvidia_detected": nv, "nvidia_detected": nv,
"nvenc_available": enc, "nvenc_available": enc,
} }
with _gpu_status_lock:
_gpu_status_cache = (time.time(), payload)
return dict(payload)
def machine_summary_payload() -> dict[str, Any]: def machine_summary_payload() -> dict[str, Any]:

View File

@@ -351,7 +351,13 @@ def _build_checks(
if client_lang and d.get("country_code"): if client_lang and d.get("country_code"):
lang = client_lang.lower() lang = client_lang.lower()
cc = (d.get("country_code") or "").lower() cc = (d.get("country_code") or "").lower()
lang_ok = (cc == "cn" and lang.startswith("zh")) or (cc == "us" and lang.startswith("en")) or True if cc == "cn":
lang_ok = lang.startswith("zh")
elif cc == "us":
lang_ok = lang.startswith("en")
else:
# 非中美地区不做严格语言判定,避免误报。
lang_ok = True
add("lang_soft", "浏览器语言与 IP 地区不冲突", lang_ok, client_lang) add("lang_soft", "浏览器语言与 IP 地区不冲突", lang_ok, client_lang)
if webrtc_leak is not None: if webrtc_leak is not None:

177
web2_network_routes.py Normal file
View File

@@ -0,0 +1,177 @@
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
router = APIRouter()
_PROFILE_TTL_SECONDS = 300.0
_QUALITY_TTL_SECONDS = 600.0
_NETWORK_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
_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
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",
ttl=_NETWORK_TTL_SECONDS,
force_refresh=force_refresh,
fetcher=run_network_dashboard,
store=_store_network,
load=_load_network,
)
@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("/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)