重构无人直播助手:内嵌 youLIVE、摄像头直推、推流诊断与 UI 焕新
将 README 改写为产品文档;移除微信/EasyTier/工作台等遗留模块;新增 youLIVE 目标端监控、摄像头推流、质量守卫与转播历史能力。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
393
web2.py
393
web2.py
@@ -28,29 +28,48 @@ from web2_youtube_oauth import (
|
||||
oauth_exchange_code,
|
||||
oauth_status as youtube_oauth_status_snapshot,
|
||||
)
|
||||
from web2_wechat_chatbot import register_wechat_chatbot_routes
|
||||
from web2_client_overview import build_client_overview
|
||||
from web2_process_runtime import summarize_process_activity
|
||||
from web2_config_governance import (
|
||||
list_config_backups,
|
||||
restore_config_backup,
|
||||
write_config_text,
|
||||
)
|
||||
from web2_quality_guard import build_youtube_quality_guard_report
|
||||
from web2_youtube_diagnostics import build_youtube_diagnostics_report
|
||||
from web2_relay_log import (
|
||||
import_relay_sessions_from_log,
|
||||
list_relay_sessions,
|
||||
relay_db_path,
|
||||
sync_relay_from_recent_log,
|
||||
track_process_status_transition,
|
||||
)
|
||||
from web2_relay_pro import (
|
||||
add_relay_channel,
|
||||
build_relay_live_status,
|
||||
ensure_default_relay_config,
|
||||
extract_url_lines_from_ini,
|
||||
first_youtube_pm2_name,
|
||||
channel_configured_for_monitor,
|
||||
get_channel_by_id,
|
||||
get_channel_detail,
|
||||
get_single_youtube_watch_url,
|
||||
get_youtube_watch_url,
|
||||
list_channels_for_pro,
|
||||
materialize_relay_channel_youtube_ini,
|
||||
read_url_config_for_channel,
|
||||
relay_pm2_name_for_channel,
|
||||
remove_relay_channel,
|
||||
set_youtube_watch_url,
|
||||
sync_channel_to_legacy_files,
|
||||
update_channel_key,
|
||||
update_channel_keys_list,
|
||||
watch_url_for_relay_process,
|
||||
write_url_config_for_channel,
|
||||
)
|
||||
from web2_youtube_ini import parse_youtube_ini, serialize_youtube_ini, validate_youtube_keys
|
||||
from web2_host_caps import gpu_status_payload, machine_summary_payload
|
||||
from web2_douyin_preview import fetch_douyin_anchor_name
|
||||
from web2_camera import camera_config_ready, list_capture_devices, load_camera_config, save_camera_config
|
||||
from web2_pocketbase import REQUIRE_PB, pb_auth_refresh_and_validate
|
||||
|
||||
|
||||
@@ -66,12 +85,20 @@ app = FastAPI(title="多进程录制控制台", lifespan=lifespan)
|
||||
BASE_DIR = Path(__file__).parent
|
||||
CONFIG_DIR = BASE_DIR / "config"
|
||||
DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs"
|
||||
RELAY_DB = relay_db_path(BASE_DIR)
|
||||
_PROCESS_STATUS_CACHE: dict[str, str] = {}
|
||||
|
||||
MAX_LOG_LINES = 300
|
||||
|
||||
|
||||
def _label_short(prefix: str) -> str:
|
||||
return {"tiktok": "TikTok", "youtube": "YouTube", "obs": "OBS", "web": "Web 控制台"}.get(prefix, prefix)
|
||||
return {
|
||||
"tiktok": "TikTok",
|
||||
"youtube": "YouTube",
|
||||
"camera": "摄像头",
|
||||
"obs": "OBS",
|
||||
"web": "Web 控制台",
|
||||
}.get(prefix, prefix)
|
||||
|
||||
|
||||
def discover_script_entries() -> tuple:
|
||||
@@ -88,6 +115,10 @@ 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")})
|
||||
|
||||
for path in sorted(base.glob("obs*.sh")):
|
||||
if path.is_file():
|
||||
entries.append({"script": path.name, "label": _label_short("obs")})
|
||||
@@ -112,6 +143,10 @@ def _stem(script: str) -> str:
|
||||
return Path(script).stem
|
||||
|
||||
|
||||
def is_camera_script(script: str) -> bool:
|
||||
return script.endswith(".py") and _stem(script).startswith("camera")
|
||||
|
||||
|
||||
def is_youtube_script(script: str) -> bool:
|
||||
return script.endswith(".py") and _stem(script).startswith("youtube")
|
||||
|
||||
@@ -242,12 +277,9 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
register_wechat_chatbot_routes(app, BASE_DIR)
|
||||
|
||||
|
||||
@app.get("/client/overview")
|
||||
async def client_overview():
|
||||
"""Electron 工作台:频道列表、微信摘要。"""
|
||||
"""Electron 工作台:频道列表摘要。"""
|
||||
return build_client_overview(CONFIG_DIR, BASE_DIR)
|
||||
|
||||
|
||||
@@ -300,6 +332,28 @@ async def relay_pro_channel_detail(channel_id: str = Query(..., description="频
|
||||
return d
|
||||
|
||||
|
||||
@app.get("/relay_pro/youtube_watch")
|
||||
async def relay_pro_youtube_watch_get(channel_id: str = Query("single", description="频道 id;单路填 single")):
|
||||
cid = str(channel_id or "single").strip() or "single"
|
||||
return {"channelId": cid, "url": get_youtube_watch_url(CONFIG_DIR, cid)}
|
||||
|
||||
|
||||
@app.post("/relay_pro/youtube_watch")
|
||||
async def relay_pro_youtube_watch_post(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
if not isinstance(data, dict):
|
||||
return JSONResponse({"error": "无效数据"}, status_code=400)
|
||||
cid = str(data.get("channelId") or data.get("channel_id") or "single").strip() or "single"
|
||||
url = str(data.get("url") or data.get("youtubeWatchUrl") or "")
|
||||
ok, msg = set_youtube_watch_url(CONFIG_DIR, cid, url)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg, "channelId": cid, "url": get_youtube_watch_url(CONFIG_DIR, cid)}
|
||||
|
||||
|
||||
@app.get("/relay_pro/url_config")
|
||||
async def relay_pro_get_url_config(channel_id: str = Query(...)):
|
||||
return {"content": read_url_config_for_channel(CONFIG_DIR, channel_id)}
|
||||
@@ -481,33 +535,129 @@ def _ffmpeg_pids_for_process(info: dict, process_name: str) -> list[int]:
|
||||
return pids
|
||||
|
||||
|
||||
def _business_status_from_runtime(
|
||||
*,
|
||||
process_status: str,
|
||||
recent_log: str,
|
||||
recent_error: str,
|
||||
ffmpeg_pids: list[int],
|
||||
log_age_seconds: float | None,
|
||||
) -> tuple[str, str]:
|
||||
log = (recent_log or "").lower()
|
||||
err = (recent_error or "").lower()
|
||||
if process_status in {"invalid", "not_found", "stopped"}:
|
||||
return "stopped", "进程未运行"
|
||||
if process_status not in {"online", "running"}:
|
||||
return "unknown", "等待进程状态稳定"
|
||||
if "至少需要一个有效" in recent_log or "串流密钥" in recent_log or "youtube key missing" in log:
|
||||
return "misconfigured", "缺少或无法使用 YouTube 串流密钥"
|
||||
if any(x in err for x in ("traceback", "error", "exception", "failed", "errno")):
|
||||
return "error", "近期错误日志中出现异常"
|
||||
if ffmpeg_pids:
|
||||
return "streaming", f"检测到 {len(ffmpeg_pids)} 个 FFmpeg 推流进程"
|
||||
if "[relay_meta]" in log or "正在直播中" in recent_log:
|
||||
return "source_live", "源站已开播,但暂未检测到 FFmpeg worker"
|
||||
if log_age_seconds is not None and log_age_seconds > 180:
|
||||
return "stale", f"日志 {int(log_age_seconds)} 秒未更新"
|
||||
if "等待直播" in recent_log or "检测直播间中" in recent_log:
|
||||
return "waiting_source", "正在等待源站开播"
|
||||
return "monitoring", "进程在线,正在监测源站"
|
||||
def _config_rel_for_channel(channel_id: str, kind: str) -> str:
|
||||
if kind == "url":
|
||||
return f"relay_pro/url_config.ch_{channel_id}.ini"
|
||||
return "youtube.ini"
|
||||
|
||||
|
||||
def _url_lines_for_process(process: str) -> str:
|
||||
cid = relay_channel_id_from_pm2_name(process)
|
||||
if cid:
|
||||
return read_url_config_for_channel(CONFIG_DIR, cid)
|
||||
p = CONFIG_DIR / "URL_config.ini"
|
||||
if p.is_file():
|
||||
try:
|
||||
return p.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _youtube_ini_text_for_process(process: str) -> str:
|
||||
cid = relay_channel_id_from_pm2_name(process)
|
||||
if cid:
|
||||
d = get_channel_detail(CONFIG_DIR, cid)
|
||||
key = (d or {}).get("youtubeKey") or ""
|
||||
return f"[youtube]\nkey = {key}\n"
|
||||
p = CONFIG_DIR / "youtube.ini"
|
||||
if p.is_file():
|
||||
try:
|
||||
return p.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _config_snapshot_for_process(process: str) -> dict[str, str]:
|
||||
return {
|
||||
"url_lines": _url_lines_for_process(process),
|
||||
"youtube_ini_text": _youtube_ini_text_for_process(process),
|
||||
"watch_url": watch_url_for_relay_process(CONFIG_DIR, process),
|
||||
}
|
||||
|
||||
|
||||
def _youtube_guard_targets(process: str) -> tuple[list[str], JSONResponse | None]:
|
||||
ensure_default_relay_config(CONFIG_DIR)
|
||||
proc = (process or "").strip()
|
||||
if proc:
|
||||
if not process_valid(proc):
|
||||
return [], JSONResponse({"error": "无效进程名"}, status_code=400)
|
||||
return [proc], None
|
||||
targets: list[str] = []
|
||||
for p in PROCESS_MONITOR:
|
||||
sc = p.get("script") or ""
|
||||
if is_youtube_script(sc):
|
||||
targets.append(p["pm2"])
|
||||
doc = list_channels_for_pro(CONFIG_DIR)
|
||||
for ch in (doc.get("channels") or []) if doc.get("ok") else []:
|
||||
cid = str(ch.get("id") or "").strip()
|
||||
if not cid or not channel_configured_for_monitor(CONFIG_DIR, cid):
|
||||
continue
|
||||
pname = relay_pm2_name_for_channel(cid)
|
||||
if pname not in targets:
|
||||
targets.append(pname)
|
||||
return targets, None
|
||||
|
||||
|
||||
async def _youtube_guard_rows(targets: list[str]) -> tuple[list[dict], dict[str, dict[str, str]]]:
|
||||
rows: list[dict] = []
|
||||
config_by_process: dict[str, dict[str, str]] = {}
|
||||
for proc in targets:
|
||||
row = await _status_payload_for_process(proc)
|
||||
label = proc
|
||||
for p in PROCESS_MONITOR:
|
||||
if p["pm2"] == proc:
|
||||
label = p.get("label") or proc
|
||||
break
|
||||
cid = relay_channel_id_from_pm2_name(proc)
|
||||
if cid:
|
||||
d = get_channel_detail(CONFIG_DIR, cid)
|
||||
if d:
|
||||
label = str(d.get("name") or label)
|
||||
row["label"] = label
|
||||
rows.append(row)
|
||||
config_by_process[proc] = _config_snapshot_for_process(proc)
|
||||
return rows, config_by_process
|
||||
|
||||
|
||||
async def _status_payload_for_process(process: str) -> dict:
|
||||
info = await control_get_process_info(process)
|
||||
recent_log = await read_log_path(info["out_path"], MAX_LOG_LINES)
|
||||
recent_error = await read_log_path(info["err_path"], MAX_LOG_LINES)
|
||||
if info["process_status"] == "not_found":
|
||||
recent_log = "进程未运行或未由本机管理器启动。\n"
|
||||
recent_error = ""
|
||||
ffmpeg_pids = _ffmpeg_pids_for_process(info, process)
|
||||
out_age = _log_age_seconds(info.get("out_path"))
|
||||
err_age = _log_age_seconds(info.get("err_path"))
|
||||
ages = [x for x in (out_age, err_age) if x is not None]
|
||||
log_age_seconds = min(ages) if ages else None
|
||||
activity = summarize_process_activity(
|
||||
process,
|
||||
str(info["process_status"]),
|
||||
recent_log,
|
||||
recent_error,
|
||||
log_age_seconds=log_age_seconds,
|
||||
ffmpeg_pids=ffmpeg_pids,
|
||||
)
|
||||
return {
|
||||
"process": process,
|
||||
"process_status": info["process_status"],
|
||||
"recent_log": recent_log,
|
||||
"recent_error": recent_error,
|
||||
"log_age_seconds": log_age_seconds,
|
||||
"ffmpeg_pids": activity["ffmpeg_pids"],
|
||||
"ffmpeg_active": activity.get("ffmpeg_active"),
|
||||
"business_status": activity["business_status"],
|
||||
"business_note": activity["business_note"],
|
||||
"is_pushing": activity["is_pushing"],
|
||||
"ffmpeg_frame": activity.get("ffmpeg_frame"),
|
||||
"ffmpeg_time_seconds": activity.get("ffmpeg_time_seconds"),
|
||||
"ffmpeg_speed": activity.get("ffmpeg_speed"),
|
||||
"ffmpeg_progress_stalled": activity.get("ffmpeg_progress_stalled"),
|
||||
"ffmpeg_speed_low": activity.get("ffmpeg_speed_low"),
|
||||
}
|
||||
|
||||
|
||||
# ---------------- 配置路由(youtube.ini,仅 youtube) ----------------
|
||||
@@ -538,10 +688,9 @@ async def save_config(request: Request, process: str = Query(..., description="
|
||||
except:
|
||||
return JSONResponse({"error": "无效的 JSON 数据"}, status_code=400)
|
||||
|
||||
config_file = CONFIG_DIR / "youtube.ini"
|
||||
try:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
config_file.write_text(content, encoding="utf-8")
|
||||
write_config_text(CONFIG_DIR, "youtube.ini", content, reason="save_config")
|
||||
return {"message": "配置保存成功"}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
@@ -593,10 +742,9 @@ async def youtube_ini_model_post(request: Request, process: str = Query(..., des
|
||||
if not isinstance(header, str):
|
||||
header = ""
|
||||
content = serialize_youtube_ini(keys, active_index, options, header)
|
||||
config_file = CONFIG_DIR / "youtube.ini"
|
||||
try:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
config_file.write_text(content, encoding="utf-8")
|
||||
write_config_text(CONFIG_DIR, "youtube.ini", content, reason="ini_model")
|
||||
return {"message": "配置保存成功"}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
@@ -630,7 +778,7 @@ async def save_url_config(request: Request, process: str = Query(..., descriptio
|
||||
config_file = CONFIG_DIR / "URL_config.ini"
|
||||
try:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
config_file.write_text(content, encoding="utf-8")
|
||||
write_config_text(CONFIG_DIR, "URL_config.ini", content, reason="save_url_config")
|
||||
return {"message": "配置保存成功"}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
@@ -701,6 +849,37 @@ def _relay_channel_ready_message(channel_id: str) -> str | None:
|
||||
|
||||
|
||||
# ---------------- PM2 控制路由 ----------------
|
||||
def _camera_ready_message(process: str) -> str | None:
|
||||
if not is_camera_script(script_for_pm2(process) or ""):
|
||||
return None
|
||||
return camera_config_ready(CONFIG_DIR)
|
||||
|
||||
|
||||
@app.get("/camera/devices")
|
||||
async def camera_devices():
|
||||
return list_capture_devices()
|
||||
|
||||
|
||||
@app.get("/camera/config")
|
||||
async def camera_config_get():
|
||||
return {"config": load_camera_config(CONFIG_DIR)}
|
||||
|
||||
|
||||
@app.post("/camera/config")
|
||||
async def camera_config_post(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
if not isinstance(data, dict):
|
||||
return JSONResponse({"error": "无效数据"}, status_code=400)
|
||||
patch = data.get("config") if isinstance(data.get("config"), dict) else data
|
||||
ok, msg = save_camera_config(CONFIG_DIR, patch)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg, "config": load_camera_config(CONFIG_DIR)}
|
||||
|
||||
|
||||
@app.get("/start")
|
||||
async def start(request: Request, process: str = Query(..., description="进程名")):
|
||||
if REQUIRE_PB:
|
||||
@@ -723,6 +902,9 @@ async def start(request: Request, process: str = Query(..., description="进程
|
||||
if not ok:
|
||||
return JSONResponse({"output": msg}, status_code=400)
|
||||
await _stop_legacy_youtube_if_online()
|
||||
cam_msg = _camera_ready_message(process)
|
||||
if cam_msg:
|
||||
return JSONResponse({"output": cam_msg}, status_code=400)
|
||||
output = await control_start(process)
|
||||
return JSONResponse({"output": output})
|
||||
|
||||
@@ -781,6 +963,9 @@ async def restart(request: Request, process: str = Query(..., description="进
|
||||
if not ok:
|
||||
return JSONResponse({"output": f"{stop_output}\n{msg}"}, status_code=400)
|
||||
await _stop_legacy_youtube_if_online()
|
||||
cam_msg = _camera_ready_message(process)
|
||||
if cam_msg:
|
||||
return JSONResponse({"output": f"{stop_output}\n{cam_msg}"}, status_code=400)
|
||||
start_output = await control_start(process)
|
||||
return JSONResponse({"output": f"{stop_output}\n{start_output}"})
|
||||
|
||||
@@ -797,37 +982,34 @@ async def status(process: str = Query(..., description="进程名")):
|
||||
|
||||
full_status_raw = await control_full_status()
|
||||
full_status = strip_ansi_codes(full_status_raw)
|
||||
info = await control_get_process_info(process)
|
||||
recent_log = await read_log_path(info["out_path"], MAX_LOG_LINES)
|
||||
recent_error = await read_log_path(info["err_path"], MAX_LOG_LINES)
|
||||
payload = await _status_payload_for_process(process)
|
||||
ps = str(payload["process_status"])
|
||||
|
||||
if info["process_status"] == "not_found":
|
||||
recent_log = "进程未运行或未由本机管理器启动。\n"
|
||||
recent_error = ""
|
||||
prev = _PROCESS_STATUS_CACHE.get(process)
|
||||
if prev != ps:
|
||||
track_process_status_transition(process, prev or "", ps, db_path=RELAY_DB)
|
||||
_PROCESS_STATUS_CACHE[process] = ps
|
||||
|
||||
ffmpeg_pids = _ffmpeg_pids_for_process(info, process)
|
||||
out_age = _log_age_seconds(info.get("out_path"))
|
||||
err_age = _log_age_seconds(info.get("err_path"))
|
||||
ages = [x for x in (out_age, err_age) if x is not None]
|
||||
log_age_seconds = min(ages) if ages else None
|
||||
business_status, business_note = _business_status_from_runtime(
|
||||
process_status=str(info["process_status"]),
|
||||
recent_log=recent_log,
|
||||
recent_error=recent_error,
|
||||
ffmpeg_pids=ffmpeg_pids,
|
||||
log_age_seconds=log_age_seconds,
|
||||
)
|
||||
url_lines = _url_lines_for_process(process)
|
||||
channel_name = ""
|
||||
cid = relay_channel_id_from_pm2_name(process)
|
||||
if cid:
|
||||
d = get_channel_detail(CONFIG_DIR, cid)
|
||||
channel_name = str((d or {}).get("name") or "")
|
||||
try:
|
||||
sync_relay_from_recent_log(
|
||||
process,
|
||||
str(payload.get("recent_log") or ""),
|
||||
channel_name=channel_name,
|
||||
url_lines=url_lines,
|
||||
db_path=RELAY_DB,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return JSONResponse({
|
||||
"raw_status": full_status,
|
||||
"process_status": info["process_status"],
|
||||
"recent_log": recent_log,
|
||||
"recent_error": recent_error,
|
||||
"business_status": business_status,
|
||||
"business_note": business_note,
|
||||
"ffmpeg_pids": ffmpeg_pids,
|
||||
"log_age_seconds": log_age_seconds,
|
||||
"is_pushing": business_status == "streaming",
|
||||
**payload,
|
||||
})
|
||||
|
||||
|
||||
@@ -931,6 +1113,85 @@ async def youtube_stream_fetch_key(process: str = Query(..., description="进程
|
||||
|
||||
|
||||
# ---------------- 进程列表(供前端同步,由 discover_script_entries 自动扫描) ----------------
|
||||
@app.get("/youtube/quality_guard")
|
||||
async def youtube_quality_guard(process: str = Query("", description="进程名,空则汇总全部 YouTube 线路")):
|
||||
targets, err = _youtube_guard_targets(process)
|
||||
if err is not None:
|
||||
return err
|
||||
rows, config_by_process = await _youtube_guard_rows(targets)
|
||||
return build_youtube_quality_guard_report(rows, config_by_process=config_by_process)
|
||||
|
||||
|
||||
@app.get("/youtube/diagnostics")
|
||||
async def youtube_diagnostics(process: str = Query("", description="进程名,空则汇总全部 YouTube 线路")):
|
||||
targets, err = _youtube_guard_targets(process)
|
||||
if err is not None:
|
||||
return err
|
||||
rows, config_by_process = await _youtube_guard_rows(targets)
|
||||
return build_youtube_diagnostics_report(rows, config_by_process=config_by_process)
|
||||
|
||||
|
||||
@app.get("/youtube/relay/sessions")
|
||||
async def youtube_relay_sessions(
|
||||
process: str = Query("", description="PM2 进程名"),
|
||||
q: str = Query("", description="搜索主播/URL/房间"),
|
||||
status: str = Query("", description="active | ended"),
|
||||
limit: int = Query(30, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
return list_relay_sessions(
|
||||
process=process,
|
||||
q=q,
|
||||
status=status,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
db_path=RELAY_DB,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/youtube/relay/sessions/import")
|
||||
async def youtube_relay_sessions_import(process: str = Query(..., description="进程名")):
|
||||
if not process_valid(process):
|
||||
return JSONResponse({"error": "无效进程名"}, status_code=400)
|
||||
info = await control_get_process_info(process)
|
||||
log_path = info.get("out_path") or ""
|
||||
if not log_path:
|
||||
return JSONResponse({"error": "无法定位 PM2 日志路径"}, status_code=400)
|
||||
result = import_relay_sessions_from_log(
|
||||
Path(log_path),
|
||||
process=process,
|
||||
url_lines=_url_lines_for_process(process),
|
||||
db_path=RELAY_DB,
|
||||
)
|
||||
if result.get("error"):
|
||||
return JSONResponse(result, status_code=400)
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/config/backups")
|
||||
async def config_backups(file: str = Query(..., description="相对 config 的路径,如 youtube.ini")):
|
||||
rel = file.strip().replace("\\", "/").lstrip("/")
|
||||
if ".." in rel.split("/"):
|
||||
return JSONResponse({"error": "非法路径"}, status_code=400)
|
||||
return {"file": rel, "backups": list_config_backups(CONFIG_DIR, rel)}
|
||||
|
||||
|
||||
@app.post("/config/restore")
|
||||
async def config_restore(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
rel = str(data.get("file") or "").strip().replace("\\", "/").lstrip("/")
|
||||
backup_id = str(data.get("backupId") or data.get("backup_id") or "").strip()
|
||||
if not rel or not backup_id or ".." in rel.split("/"):
|
||||
return JSONResponse({"error": "参数无效"}, status_code=400)
|
||||
ok, msg = restore_config_backup(CONFIG_DIR, rel, backup_id)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
@app.get("/process_monitor")
|
||||
async def process_monitor():
|
||||
ensure_default_relay_config(CONFIG_DIR)
|
||||
@@ -942,7 +1203,7 @@ async def process_monitor():
|
||||
if doc.get("ok"):
|
||||
for ch in doc.get("channels") or []:
|
||||
cid = str(ch.get("id") or "").strip()
|
||||
if not cid:
|
||||
if not cid or not channel_configured_for_monitor(CONFIG_DIR, cid):
|
||||
continue
|
||||
entries.append(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user