s
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user