网络页焕新:IP 画像/质量检测、链路诊断与功能开关

新增 /ip_profile、/ip_quality API 与网络 UI 组件,默认隐藏摄像头/拉流导航;同步更新 README。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-08 12:47:53 +08:00
parent 119e52cfe9
commit ae9531f2c2
23 changed files with 6966 additions and 543 deletions

64
web2.py
View File

@@ -20,6 +20,8 @@ from web2_supervisor import (
use_builtin,
)
from web2_network import run_network_dashboard
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_system_metrics import snapshot as system_metrics_snapshot
from web2_youtube_oauth import (
@@ -77,6 +79,11 @@ from web2_pull_stream import (
save_pull_stream_config,
)
from web2_pocketbase import REQUIRE_PB, pb_auth_refresh_and_validate
from web2_feature_flags import (
SHOW_CAMERA_FEATURE,
SHOW_PULL_STREAM_FEATURE,
is_disabled_stream_worker_pm2,
)
@asynccontextmanager
@@ -122,13 +129,15 @@ def discover_script_entries() -> tuple:
if path.is_file() and path.resolve() != web_path:
entries.append({"script": path.name, "label": _label_short("youtube")})
for path in sorted(base.glob("camera*.py")):
if path.is_file() and path.resolve() != web_path:
entries.append({"script": path.name, "label": _label_short("camera")})
if SHOW_CAMERA_FEATURE:
for path in sorted(base.glob("camera*.py")):
if path.is_file() and path.resolve() != web_path:
entries.append({"script": path.name, "label": _label_short("camera")})
for path in sorted(base.glob("pull_stream*.py")):
if path.is_file() and path.resolve() != web_path:
entries.append({"script": path.name, "label": _label_short("pull")})
if SHOW_PULL_STREAM_FEATURE:
for path in sorted(base.glob("pull_stream*.py")):
if path.is_file() and path.resolve() != web_path:
entries.append({"script": path.name, "label": _label_short("pull")})
for path in sorted(base.glob("obs*.sh")):
if path.is_file():
@@ -193,6 +202,8 @@ def relay_channel_id_from_pm2_name(name: str) -> Optional[str]:
def process_valid(name: str) -> bool:
if is_disabled_stream_worker_pm2(name):
return False
if name in VALID_PROCESSES:
return True
cid = relay_channel_id_from_pm2_name(name)
@@ -880,18 +891,28 @@ def _stream_worker_ready_message(process: str) -> str | None:
return _camera_ready_message(process) or _pull_stream_ready_message(process)
def _disabled_feature_response(feature: str) -> JSONResponse:
return JSONResponse({"error": f"{feature} 功能已禁用"}, status_code=403)
@app.get("/camera/devices")
async def camera_devices():
if not SHOW_CAMERA_FEATURE:
return _disabled_feature_response("摄像头")
return list_capture_devices()
@app.get("/camera/config")
async def camera_config_get():
if not SHOW_CAMERA_FEATURE:
return _disabled_feature_response("摄像头")
return {"config": load_camera_config(CONFIG_DIR)}
@app.post("/camera/config")
async def camera_config_post(request: Request):
if not SHOW_CAMERA_FEATURE:
return _disabled_feature_response("摄像头")
try:
data = await request.json()
except Exception:
@@ -907,16 +928,22 @@ async def camera_config_post(request: Request):
@app.get("/pull_stream/config")
async def pull_stream_config_get():
if not SHOW_PULL_STREAM_FEATURE:
return _disabled_feature_response("拉流")
return {"config": load_pull_stream_config(CONFIG_DIR)}
@app.get("/pull_stream/protocols")
async def pull_stream_protocols():
if not SHOW_PULL_STREAM_FEATURE:
return _disabled_feature_response("拉流")
return {"protocols": protocol_help()}
@app.post("/pull_stream/config")
async def pull_stream_config_post(request: Request):
if not SHOW_PULL_STREAM_FEATURE:
return _disabled_feature_response("拉流")
try:
data = await request.json()
except Exception:
@@ -1077,6 +1104,31 @@ async def network_status():
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")
async def get_proxy_config():
"""仅作用于录制子进程的代理(不写 Windows 系统代理)。"""