'init'
Some checks failed
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
Some checks failed
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
This commit is contained in:
565
web2.py
Normal file
565
web2.py
Normal file
@@ -0,0 +1,565 @@
|
||||
from fastapi import FastAPI, Query, Request
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import html
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from web2_supervisor import (
|
||||
BuiltinSupervisor,
|
||||
force_kill_ffmpeg_cross_platform,
|
||||
get_process_info_pm2,
|
||||
run_pm2_shell,
|
||||
strip_ansi_codes,
|
||||
use_builtin,
|
||||
)
|
||||
from web2_network import run_network_dashboard
|
||||
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 (
|
||||
fetch_stream_key_and_write_ini,
|
||||
oauth_authorization_url,
|
||||
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_relay_pro import (
|
||||
build_relay_live_status,
|
||||
ensure_default_relay_config,
|
||||
first_youtube_pm2_name,
|
||||
get_channel_detail,
|
||||
list_channels_for_pro,
|
||||
read_url_config_for_channel,
|
||||
sync_channel_to_legacy_files,
|
||||
update_channel_key,
|
||||
write_url_config_for_channel,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
ensure_default_relay_config(CONFIG_DIR)
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="多进程录制控制台", lifespan=lifespan)
|
||||
|
||||
# ---------------- 配置(根目录结构) ----------------
|
||||
BASE_DIR = Path(__file__).parent
|
||||
CONFIG_DIR = BASE_DIR / "config"
|
||||
DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs"
|
||||
|
||||
MAX_LOG_LINES = 300
|
||||
|
||||
|
||||
def _label_short(prefix: str) -> str:
|
||||
return {"tiktok": "TikTok", "youtube": "YouTube", "obs": "OBS", "web": "Web 控制台"}.get(prefix, prefix)
|
||||
|
||||
|
||||
def discover_script_entries() -> tuple:
|
||||
"""扫描项目根目录自动识别入口:tiktok*.py、youtube*.py、obs*.sh / obs*.bat,以及当前本文件(Web 控制台)。进程名 = 主文件名(无扩展名)。"""
|
||||
base = BASE_DIR
|
||||
web_path = Path(__file__).resolve()
|
||||
entries: list[dict] = []
|
||||
|
||||
for path in sorted(base.glob("tiktok*.py")):
|
||||
if path.is_file() and path.resolve() != web_path:
|
||||
entries.append({"script": path.name, "label": _label_short("tiktok")})
|
||||
|
||||
for path in sorted(base.glob("youtube*.py")):
|
||||
if path.is_file() and path.resolve() != web_path:
|
||||
entries.append({"script": path.name, "label": _label_short("youtube")})
|
||||
|
||||
for path in sorted(base.glob("obs*.sh")):
|
||||
if path.is_file():
|
||||
entries.append({"script": path.name, "label": _label_short("obs")})
|
||||
|
||||
for path in sorted(base.glob("obs*.bat")):
|
||||
if path.is_file():
|
||||
entries.append({"script": path.name, "label": _label_short("obs")})
|
||||
|
||||
entries.append({"script": web_path.name, "label": _label_short("web")})
|
||||
|
||||
return tuple(entries)
|
||||
|
||||
|
||||
SCRIPT_ENTRIES = discover_script_entries()
|
||||
|
||||
|
||||
def pm2_name_from_script(script: str) -> str:
|
||||
return Path(script).stem
|
||||
|
||||
|
||||
def _stem(script: str) -> str:
|
||||
return Path(script).stem
|
||||
|
||||
|
||||
def is_youtube_script(script: str) -> bool:
|
||||
return script.endswith(".py") and _stem(script).startswith("youtube")
|
||||
|
||||
|
||||
def is_tiktok_script(script: str) -> bool:
|
||||
return script.endswith(".py") and _stem(script).startswith("tiktok")
|
||||
|
||||
|
||||
def _build_process_monitor():
|
||||
return tuple(
|
||||
{
|
||||
"pm2": pm2_name_from_script(e["script"]),
|
||||
"script": e["script"],
|
||||
"label": e["label"],
|
||||
}
|
||||
for e in SCRIPT_ENTRIES
|
||||
)
|
||||
|
||||
|
||||
PROCESS_MONITOR = _build_process_monitor()
|
||||
VALID_PROCESSES = frozenset(p["pm2"] for p in PROCESS_MONITOR)
|
||||
|
||||
URL_CONFIG_PM2 = frozenset(
|
||||
pm2_name_from_script(e["script"])
|
||||
for e in SCRIPT_ENTRIES
|
||||
if is_tiktok_script(e["script"]) or is_youtube_script(e["script"])
|
||||
)
|
||||
|
||||
|
||||
def script_for_pm2(pm2: str) -> Optional[str]:
|
||||
for p in PROCESS_MONITOR:
|
||||
if p["pm2"] == pm2:
|
||||
return p["script"]
|
||||
return None
|
||||
|
||||
|
||||
WEB_SCRIPT_NAME = Path(__file__).name
|
||||
_builtin_supervisor: Optional[BuiltinSupervisor] = None
|
||||
|
||||
|
||||
def get_builtin_supervisor() -> BuiltinSupervisor:
|
||||
global _builtin_supervisor
|
||||
if _builtin_supervisor is None:
|
||||
_builtin_supervisor = BuiltinSupervisor(BASE_DIR, WEB_SCRIPT_NAME, script_for_pm2)
|
||||
return _builtin_supervisor
|
||||
|
||||
|
||||
async def control_start(process: str) -> str:
|
||||
if use_builtin():
|
||||
return await get_builtin_supervisor().start(process)
|
||||
return await run_pm2_shell(f"start {process}")
|
||||
|
||||
|
||||
async def control_stop(process: str) -> str:
|
||||
if use_builtin():
|
||||
return await get_builtin_supervisor().stop(process)
|
||||
return await run_pm2_shell(f"stop {process}")
|
||||
|
||||
|
||||
async def control_restart(process: str) -> str:
|
||||
if use_builtin():
|
||||
return await get_builtin_supervisor().restart(process)
|
||||
return await run_pm2_shell(f"restart {process}")
|
||||
|
||||
|
||||
async def control_full_status() -> str:
|
||||
if use_builtin():
|
||||
return await get_builtin_supervisor().full_status_text()
|
||||
return await run_pm2_shell("status", timeout=8.0)
|
||||
|
||||
|
||||
async def control_get_process_info(process: str) -> dict:
|
||||
if use_builtin():
|
||||
return await get_builtin_supervisor().get_process_info(process)
|
||||
return await get_process_info_pm2(process, DEFAULT_LOG_DIR, strip_ansi_codes)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def web2_token_middleware(request: Request, call_next):
|
||||
"""若设置环境变量 WEB2_API_TOKEN,则除 /health 与 OpenAPI 外需携带 Bearer 或 X-API-Token。"""
|
||||
# CORS 预检不带 Authorization;若在外层先于 CORSMiddleware 拦截会缺少 Access-Control-* 头
|
||||
if request.method == "OPTIONS":
|
||||
return await call_next(request)
|
||||
token = os.environ.get("WEB2_API_TOKEN", "").strip()
|
||||
if not token:
|
||||
return await call_next(request)
|
||||
path = request.url.path
|
||||
if path == "/health" or path.startswith("/docs") or path in ("/openapi.json", "/redoc"):
|
||||
return await call_next(request)
|
||||
auth = request.headers.get("authorization") or ""
|
||||
xtok = (request.headers.get("x-api-token") or "").strip()
|
||||
if auth.startswith("Bearer ") and auth[7:].strip() == token:
|
||||
return await call_next(request)
|
||||
if xtok == token:
|
||||
return await call_next(request)
|
||||
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
||||
|
||||
|
||||
# 最后注册,保证 CORSMiddleware 在最外层,401 等响应仍带 CORS 头(供 Vite 等跨源调试)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["GET", "POST", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
register_wechat_chatbot_routes(app, BASE_DIR)
|
||||
|
||||
|
||||
@app.get("/client/overview")
|
||||
async def client_overview():
|
||||
"""Electron 工作台:频道列表、微信摘要(EasyTier 节点由主进程 easytier-cli 查询)。"""
|
||||
return build_client_overview(CONFIG_DIR, BASE_DIR)
|
||||
|
||||
|
||||
# ---------------- Pro 多路转播(每频道独立 URL 文件 + 去重 + 同步 legacy) ----------------
|
||||
|
||||
|
||||
@app.get("/relay_pro/channels")
|
||||
async def relay_pro_channels():
|
||||
return list_channels_for_pro(CONFIG_DIR)
|
||||
|
||||
|
||||
@app.get("/relay_pro/channel_detail")
|
||||
async def relay_pro_channel_detail(channel_id: str = Query(..., description="频道 id,如 ch_0")):
|
||||
d = get_channel_detail(CONFIG_DIR, channel_id)
|
||||
if not d:
|
||||
return JSONResponse({"error": "未找到频道"}, status_code=404)
|
||||
return d
|
||||
|
||||
|
||||
@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)}
|
||||
|
||||
|
||||
@app.post("/relay_pro/url_config")
|
||||
async def relay_pro_post_url_config(request: Request, channel_id: str = Query(...)):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
content = data.get("content", "") if isinstance(data, dict) else ""
|
||||
ok, msg = write_url_config_for_channel(CONFIG_DIR, channel_id, content)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
@app.post("/relay_pro/channel_key")
|
||||
async def relay_pro_save_channel_key(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 "").strip()
|
||||
key = str(data.get("youtubeKey") or data.get("key") or "")
|
||||
if not cid:
|
||||
return JSONResponse({"error": "缺少 channelId"}, status_code=400)
|
||||
ok, msg = update_channel_key(CONFIG_DIR, cid, key)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
@app.post("/relay_pro/sync_active")
|
||||
async def relay_pro_sync_active(channel_id: str = Query(...)):
|
||||
ok, msg = sync_channel_to_legacy_files(CONFIG_DIR, channel_id)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
@app.get("/relay_pro/live_status")
|
||||
async def relay_pro_live_status():
|
||||
yt = first_youtube_pm2_name([{"script": e["script"]} for e in SCRIPT_ENTRIES])
|
||||
|
||||
async def read_tail(path: str, lines: int) -> str:
|
||||
return await read_log_path(path, lines)
|
||||
|
||||
return await build_relay_live_status(
|
||||
CONFIG_DIR,
|
||||
BASE_DIR,
|
||||
control_get_process_info,
|
||||
read_tail,
|
||||
yt,
|
||||
)
|
||||
|
||||
|
||||
# ---------------- 工具函数 ----------------
|
||||
async def read_log_path(path: str, lines: int) -> str:
|
||||
if not path:
|
||||
return "无日志路径\n"
|
||||
if "/dev/null" in path:
|
||||
return "日志输出被禁用(无日志文件)\n"
|
||||
log_file = Path(path)
|
||||
if not log_file.exists():
|
||||
return f"日志文件不存在: {path}\n"
|
||||
try:
|
||||
with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
|
||||
content_lines = f.readlines()
|
||||
return "".join(content_lines[-lines:]) if content_lines else "无日志内容\n"
|
||||
except Exception as e:
|
||||
return f"读取日志失败 ({path}): {str(e)}\n"
|
||||
|
||||
|
||||
# ---------------- 配置路由(youtube.ini,仅 youtube) ----------------
|
||||
@app.get("/get_config")
|
||||
async def get_config(process: str = Query(..., description="进程名")):
|
||||
sc = script_for_pm2(process)
|
||||
if not sc or not is_youtube_script(sc):
|
||||
return JSONResponse({"error": "仅 youtube*.py 对应进程支持 youtube.ini 编辑"}, status_code=400)
|
||||
|
||||
config_file = CONFIG_DIR / "youtube.ini"
|
||||
if not config_file.exists():
|
||||
return {"content": "# youtube.ini 文件不存在,将创建空文件\n"}
|
||||
try:
|
||||
content = config_file.read_text(encoding="utf-8")
|
||||
return {"content": content}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"读取失败: {str(e)}"}, status_code=500)
|
||||
|
||||
@app.post("/save_config")
|
||||
async def save_config(request: Request, process: str = Query(..., description="进程名")):
|
||||
sc = script_for_pm2(process)
|
||||
if not sc or not is_youtube_script(sc):
|
||||
return JSONResponse({"error": "仅 youtube*.py 对应进程支持 youtube.ini 编辑"}, status_code=400)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
content = data.get("content", "")
|
||||
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")
|
||||
return {"message": "配置保存成功"}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
|
||||
# ---------------- 配置路由(URL_config.ini,youtube 和 tiktok 共享) ----------------
|
||||
@app.get("/get_url_config")
|
||||
async def get_url_config(process: str = Query(..., description="进程名")):
|
||||
if process not in URL_CONFIG_PM2:
|
||||
return JSONResponse({"error": "仅 tiktok*.py / youtube*.py 对应进程支持 URL 配置编辑"}, status_code=400)
|
||||
|
||||
config_file = CONFIG_DIR / "URL_config.ini"
|
||||
if not config_file.exists():
|
||||
return {"content": "# URL_config.ini 文件不存在,将创建空文件\n"}
|
||||
try:
|
||||
content = config_file.read_text(encoding="utf-8")
|
||||
return {"content": content}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"读取失败: {str(e)}"}, status_code=500)
|
||||
|
||||
@app.post("/save_url_config")
|
||||
async def save_url_config(request: Request, process: str = Query(..., description="进程名")):
|
||||
if process not in URL_CONFIG_PM2:
|
||||
return JSONResponse({"error": "仅 tiktok*.py / youtube*.py 对应进程支持 URL 配置编辑"}, status_code=400)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
content = data.get("content", "")
|
||||
except:
|
||||
return JSONResponse({"error": "无效的 JSON 数据"}, status_code=400)
|
||||
|
||||
config_file = CONFIG_DIR / "URL_config.ini"
|
||||
try:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
config_file.write_text(content, encoding="utf-8")
|
||||
return {"message": "配置保存成功"}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
|
||||
# ---------------- PM2 控制路由 ----------------
|
||||
@app.get("/start")
|
||||
async def start(process: str = Query(..., description="进程名")):
|
||||
if process not in VALID_PROCESSES:
|
||||
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
||||
output = await control_start(process)
|
||||
return JSONResponse({"output": output})
|
||||
|
||||
|
||||
@app.get("/stop")
|
||||
async def stop(process: str = Query(..., description="进程名")):
|
||||
if process not in VALID_PROCESSES:
|
||||
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
||||
|
||||
# 第一步:尝试优雅停止
|
||||
ctrl_output = await control_stop(process)
|
||||
|
||||
# 第二步:等待信号传播(通常 1~3 秒)
|
||||
await asyncio.sleep(2.0)
|
||||
|
||||
# 第三步:录制类进程才强杀 ffmpeg(web* 控制台不杀 ffmpeg)
|
||||
if not process.startswith("web"):
|
||||
await force_kill_ffmpeg_cross_platform(process)
|
||||
|
||||
# 第四步:再等一小会儿,确认
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
return JSONResponse({
|
||||
"message": "已执行 stop" + ("" if process.startswith("web") else " + 强杀 ffmpeg"),
|
||||
"pm2_output": ctrl_output,
|
||||
"note": "如果仍有残留,可多次点击停止或查看下方日志",
|
||||
})
|
||||
|
||||
|
||||
@app.get("/restart")
|
||||
async def restart(process: str = Query(..., description="进程名")):
|
||||
if process not in VALID_PROCESSES:
|
||||
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
||||
output = await control_restart(process)
|
||||
if not process.startswith("web"):
|
||||
await force_kill_ffmpeg_cross_platform(process)
|
||||
return JSONResponse({"output": output})
|
||||
|
||||
|
||||
@app.get("/status")
|
||||
async def status(process: str = Query(..., description="进程名")):
|
||||
if process not in VALID_PROCESSES:
|
||||
return JSONResponse({
|
||||
"raw_status": "",
|
||||
"process_status": "invalid",
|
||||
"recent_log": f"无效进程名: {process}",
|
||||
"recent_error": ""
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
if info["process_status"] == "not_found":
|
||||
recent_log = "进程未运行或未由本机管理器启动。\n"
|
||||
recent_error = ""
|
||||
|
||||
return JSONResponse({
|
||||
"raw_status": full_status,
|
||||
"process_status": info["process_status"],
|
||||
"recent_log": recent_log,
|
||||
"recent_error": recent_error,
|
||||
})
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
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("/proxy_config")
|
||||
async def get_proxy_config():
|
||||
"""仅作用于录制子进程的代理(不写 Windows 系统代理)。"""
|
||||
return read_proxy_config()
|
||||
|
||||
|
||||
@app.post("/proxy_config")
|
||||
async def post_proxy_config(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)
|
||||
write_proxy_config(
|
||||
{
|
||||
"enabled": bool(data.get("enabled")),
|
||||
"http": str(data.get("http") or ""),
|
||||
"socks": str(data.get("socks") or ""),
|
||||
}
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/system_metrics")
|
||||
async def system_metrics():
|
||||
"""CPU/内存/磁盘/网卡速率与 ffmpeg 进程摘要。"""
|
||||
try:
|
||||
return system_metrics_snapshot()
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
# ---------------- YouTube OAuth(串流密钥) ----------------
|
||||
@app.get("/youtube/oauth/start")
|
||||
async def youtube_oauth_start():
|
||||
"""返回 Google 授权页 URL,前端应用浏览器打开。"""
|
||||
try:
|
||||
url, state = oauth_authorization_url()
|
||||
return {"url": url, "state": state}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=400)
|
||||
|
||||
|
||||
@app.get("/youtube/oauth/callback")
|
||||
async def youtube_oauth_callback(code: str = Query(...), state: str = Query(...)):
|
||||
"""Google 授权完成后的回调(须与 google_oauth.json 中 redirect_uri 一致)。"""
|
||||
try:
|
||||
oauth_exchange_code(code, state)
|
||||
except Exception as e:
|
||||
err = html.escape(str(e))
|
||||
return HTMLResponse(
|
||||
f"<!DOCTYPE html><html><head><meta charset='utf-8'><title>授权失败</title></head>"
|
||||
f"<body style='font-family:system-ui;padding:24px;background:#1a1d24;color:#f56565;'>"
|
||||
f"<p>授权失败:{err}</p><p style='color:#8b929e;font-size:13px;'>请关闭此页后重试。</p></body></html>",
|
||||
status_code=400,
|
||||
)
|
||||
return HTMLResponse(
|
||||
"<!DOCTYPE html><html><head><meta charset='utf-8'><title>授权成功</title></head>"
|
||||
"<body style='font-family:system-ui;padding:24px;background:#1a1d24;color:#e8eaed;'>"
|
||||
"<p>授权成功,可关闭此页并返回「直播录制助手」。</p>"
|
||||
"<p style='color:#8b929e;font-size:13px;'>若浏览器未自动关闭,请手动关闭本标签页。</p>"
|
||||
"</body></html>"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/youtube/oauth/status")
|
||||
async def youtube_oauth_status_route():
|
||||
return youtube_oauth_status_snapshot()
|
||||
|
||||
|
||||
@app.post("/youtube/stream/fetch_key")
|
||||
async def youtube_stream_fetch_key(process: str = Query(..., description="进程名")):
|
||||
"""调用 YouTube Live API 获取串流密钥并写入 config/youtube.ini。"""
|
||||
if process not in VALID_PROCESSES:
|
||||
return JSONResponse({"error": "无效进程名"}, status_code=400)
|
||||
sc = script_for_pm2(process)
|
||||
if not sc or not is_youtube_script(sc):
|
||||
return JSONResponse({"error": "仅 youtube*.py 对应进程可使用此功能"}, status_code=400)
|
||||
try:
|
||||
return fetch_stream_key_and_write_ini()
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
# ---------------- 进程列表(供前端同步,由 discover_script_entries 自动扫描) ----------------
|
||||
@app.get("/process_monitor")
|
||||
async def process_monitor():
|
||||
return {
|
||||
"entries": [
|
||||
{"pm2": p["pm2"], "script": p["script"], "label": p["label"]}
|
||||
for p in PROCESS_MONITOR
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ---------------- 首页 ----------------
|
||||
@app.get("/")
|
||||
async def index():
|
||||
return FileResponse(BASE_DIR / "index.html")
|
||||
Reference in New Issue
Block a user