This commit is contained in:
eric
2026-05-19 11:58:20 +00:00
parent b617555360
commit bae55ec52c
18 changed files with 136 additions and 35 deletions

View File

@@ -6,7 +6,9 @@ import json
import os
import re
import signal
import sqlite3
import sys
import time
from contextlib import asynccontextmanager, suppress
from pathlib import Path
from typing import Any, Optional
@@ -362,6 +364,10 @@ def _persist_youtube_pro_channel_edits(
return updated
def _youtube_pro_store_error(exc: sqlite3.Error) -> str:
return f"多频道状态数据库写入失败: {exc}; 请确认 config 目录归 live 用户可写"
def _validate_youtube_runtime_files(process: str) -> list[str]:
return validate_youtube_runtime_files(BASE_DIR, CONFIG_DIR, process, get_youtube_pro_channels())
@@ -553,6 +559,8 @@ async def alias_youtube_pro_channels_post(body: YoutubeProChannelsBody):
set_youtube_pro_channels(body.channels)
except ValueError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
except sqlite3.Error as exc:
return JSONResponse({"error": _youtube_pro_store_error(exc)}, status_code=500)
capacity = await asyncio.to_thread(ensure_neko_capacity_for_youtube_channels, BASE_DIR, body.channels)
return {"message": "ok", "neko": capacity}
@@ -570,6 +578,35 @@ _NO_STORE_HEADERS = {
"Expires": "0",
}
_READ_CONFIG_MIN_INTERVAL_SECONDS = 0.45
_READ_CONFIG_LAST_SEEN: dict[tuple[str, str, str], float] = {}
def _client_host(request: Request) -> str:
forwarded = (request.headers.get("x-forwarded-for") or "").split(",", 1)[0].strip()
if forwarded:
return forwarded
return request.client.host if request.client else "unknown"
def _reject_config_read_storm(request: Request, endpoint: str, process: str) -> JSONResponse | None:
now = time.monotonic()
if len(_READ_CONFIG_LAST_SEEN) > 2048:
cutoff = now - 30.0
for key, seen_at in list(_READ_CONFIG_LAST_SEEN.items()):
if seen_at < cutoff:
_READ_CONFIG_LAST_SEEN.pop(key, None)
key = (_client_host(request), endpoint, (process or "").strip())
last = _READ_CONFIG_LAST_SEEN.get(key, 0.0)
_READ_CONFIG_LAST_SEEN[key] = now
if now - last >= _READ_CONFIG_MIN_INTERVAL_SECONDS:
return None
return JSONResponse(
{"error": "刷新过快,请稍后重试"},
status_code=429,
headers={**_NO_STORE_HEADERS, "Retry-After": "1"},
)
class ProcessNameBody(BaseModel):
process: str = Field(..., min_length=1, max_length=160)
@@ -1112,7 +1149,10 @@ async def get_system_snapshot():
# ---------------- 配置路由youtube.ini仅 youtube ----------------
@app.get("/get_config")
async def get_config(process: str = Query(..., description="进程名")):
async def get_config(request: Request, process: str = Query(..., description="进程名")):
storm = _reject_config_read_storm(request, "get_config", process)
if storm is not None:
return storm
rel = managed_youtube_ini_relpath(process)
if not rel:
return JSONResponse(
@@ -1156,13 +1196,18 @@ async def save_config(request: Request, process: str = Query(..., description="
governed_save_managed_file("app-config", rel, content)
_persist_youtube_pro_channel_edits(process, youtube_ini_content=content)
return JSONResponse({"message": "配置保存成功"}, headers=_NO_STORE_HEADERS)
except sqlite3.Error as e:
return JSONResponse({"error": _youtube_pro_store_error(e)}, status_code=500)
except OSError as e:
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
# ---------------- 配置路由URL_config.ini ----------------
@app.get("/get_url_config")
async def get_url_config(process: str = Query(..., description="进程名")):
async def get_url_config(request: Request, process: str = Query(..., description="进程名")):
storm = _reject_config_read_storm(request, "get_url_config", process)
if storm is not None:
return storm
if not allows_url_config(process):
return JSONResponse(
{"error": "仅 tiktok*.py / youtube*.py / douyin_youtube*.py 对应进程支持 URL 配置编辑"},
@@ -1210,6 +1255,8 @@ async def save_url_config(request: Request, process: str = Query(..., descriptio
governed_save_managed_file("app-config", rel, content)
_persist_youtube_pro_channel_edits(process, url_content=content)
return JSONResponse({"message": "配置保存成功"}, headers=_NO_STORE_HEADERS)
except sqlite3.Error as e:
return JSONResponse({"error": _youtube_pro_store_error(e)}, status_code=500)
except OSError as e:
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)