This commit is contained in:
eric
2026-05-16 19:24:30 -05:00
parent 19beec12a5
commit 75a0ca4e31
260 changed files with 51345 additions and 1 deletions

247
d2ypp2/scripts/launch.py Normal file
View File

@@ -0,0 +1,247 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
一键启动 Web 控制台:
生产:构建 web-console若需要+ 仅 uvicorn单端口托管 API + 静态页)
开发uvicorn + Next dev默认绑定 0.0.0.0,局域网可访问)
自动加载 config/runtime.env若存在不覆盖已有环境变量
用法:
python scripts/launch.py
python scripts/launch.py --dev
python scripts/launch.py --port 8101
python scripts/launch.py --skip-build
python scripts/launch.py --no-env-check
环境变量: HOST, PORT, NEXT_DEV_PORT, WEB_STACK=api, API_PROXY_TARGET开发
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
ROOT = Path(__file__).resolve().parent.parent
SCRIPTS_DIR = Path(__file__).resolve().parent
CONSOLE = ROOT / "web-console"
OUT_INDEX = CONSOLE / "out" / "index.html"
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(SCRIPTS_DIR))
import env_check # noqa: E402
from src.runtime_bootstrap import raise_nofile_limit # noqa: E402
def load_runtime_env() -> None:
path = ROOT / "config" / "runtime.env"
if not path.is_file():
return
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
k, _, v = line.partition("=")
k, v = k.strip(), v.strip().strip('"').strip("'")
if k and k not in os.environ:
os.environ[k] = v
def _which_npm() -> str | None:
return shutil.which("npm")
def _npm(args: list[str], *, cwd: Path, env: dict | None = None) -> int:
full_env = {**os.environ, **(env or {})}
npm = _which_npm()
if not npm:
return 127
return subprocess.call([npm, *args], cwd=cwd, env=full_env)
def ensure_console_built(*, skip: bool) -> bool:
"""返回是否已存在可托管的静态 index。"""
if skip:
return OUT_INDEX.is_file()
if not CONSOLE.is_dir():
print("[launch] 未找到 web-console将仅使用根目录 index.html若存在")
return False
if not _which_npm():
print("[launch] 未检测到 npm跳过前端构建请先安装 Node.js 或手动在 web-console 执行 npm run build。")
return OUT_INDEX.is_file()
if OUT_INDEX.is_file():
return True
print("[launch] 正在安装依赖并构建 Web 控制台(首次略慢)…")
lock = CONSOLE / "package-lock.json"
if lock.is_file():
if _npm(["ci"], cwd=CONSOLE) != 0:
print("[launch] npm ci 失败,尝试 npm install…")
if _npm(["install"], cwd=CONSOLE) != 0:
sys.exit(1)
else:
if _npm(["install"], cwd=CONSOLE) != 0:
sys.exit(1)
if _npm(["run", "build"], cwd=CONSOLE) != 0:
if OUT_INDEX.is_file():
print(
"[launch] npm run build 失败,但检测到已有 web-console/out/index.html"
"将继续启动 API静态页可能为旧构建请修复构建后重启",
)
return True
print(
"[launch] npm run build 失败且缺少 web-console/out/index.html"
"无法提供控制台。请在 web-console 目录执行 npm run build 或检查 Node/内存。",
)
sys.exit(1)
print("[launch] 前端构建完成。")
return True
def _terminate_process(p: subprocess.Popen | None) -> None:
if p is None or p.poll() is not None:
return
p.terminate()
try:
p.wait(timeout=8)
except subprocess.TimeoutExpired:
p.kill()
p.wait(timeout=3)
def run_dev(host: str, port: str, next_port: str) -> None:
if not _which_npm():
print("[launch] 开发模式需要 Node.js/npm。仅起 API 请用: python scripts/launch.py --api-only --reload")
sys.exit(1)
api_url = f"http://127.0.0.1:{port}"
env = os.environ.copy()
env.setdefault("API_PROXY_TARGET", api_url)
api_cmd = [
sys.executable,
"-m",
"uvicorn",
"web:app",
"--host",
host,
"--port",
port,
"--reload",
]
os.chdir(ROOT)
api: subprocess.Popen | None = None
ui: subprocess.Popen | None = None
try:
api = subprocess.Popen(api_cmd, cwd=ROOT)
time.sleep(0.6)
if api.poll() is not None:
print("[launch] API 进程异常退出,请检查端口是否被占用。")
sys.exit(api.returncode or 1)
ui = subprocess.Popen(
["npm", "run", "dev", "--", "--hostname", "0.0.0.0", "--port", next_port],
cwd=CONSOLE,
env=env,
)
print()
print(" —— 开发模式(局域网可访问)——")
print(f" 控制台界面: http://0.0.0.0:{next_port} 或用本机 IP + 端口")
print(f" API: http://{host}:{port}/ Next 已代理同名接口到 {api_url}")
print(" 按 Ctrl+C 可同时结束前后端")
if host == "0.0.0.0":
print(" 提示: 绑定 0.0.0.0 时请勿把管理端口暴露到公网")
print()
while True:
time.sleep(0.5)
if api.poll() is not None:
print("[launch] API 已退出")
break
if ui and ui.poll() is not None:
print("[launch] Next 已退出")
break
except KeyboardInterrupt:
print("\n[launch] 正在关闭…")
finally:
_terminate_process(ui)
_terminate_process(api)
def run_api_only(host: str, port: str, reload: bool) -> None:
os.chdir(ROOT)
cmd = [sys.executable, "-m", "uvicorn", "web:app", "--host", host, "--port", port]
if reload:
cmd.append("--reload")
os.execvp(sys.executable, cmd)
def run_prod(host: str, port: str, skip_build: bool, reload: bool) -> None:
ensure_console_built(skip=skip_build)
run_api_only(host, port, reload)
def main() -> None:
load_runtime_env()
raise_nofile_limit()
default_port = os.environ.get("PORT", "8001")
ap = argparse.ArgumentParser(description="直播控制台一键启动")
ap.add_argument("--dev", action="store_true", help="开发API + Next需 Node")
ap.add_argument(
"--api-only",
action="store_true",
help="仅 uvicorn不构建、不启动 Next",
)
ap.add_argument("--host", default=None, help="监听地址(默认生产 0.0.0.0,可经 HOST 环境变量)")
ap.add_argument("--port", default=None, help="端口(默认 PORT 或 8001")
ap.add_argument("--skip-build", action="store_true", help="跳过前端构建检测")
ap.add_argument("--reload", action="store_true", help="uvicorn --reload生产慎用")
ap.add_argument("--no-env-check", action="store_true", help="跳过环境分析")
args = ap.parse_args()
if os.environ.get("WEB_STACK", "").lower() == "api":
args.api_only = True
port = str(args.port if args.port is not None else default_port)
next_port = os.environ.get("NEXT_DEV_PORT", "3000")
if args.dev:
host = args.host or os.environ.get("HOST") or "0.0.0.0"
else:
host = args.host or os.environ.get("HOST") or "0.0.0.0"
need_npm = args.dev or (
not args.skip_build and not args.api_only and not OUT_INDEX.is_file()
)
if not args.no_env_check:
env_check.print_env_report(
dev=bool(args.dev),
need_npm=need_npm,
host=host,
port=port,
next_port=next_port,
)
if args.dev:
run_dev(host, port, next_port)
return
if args.api_only:
run_api_only(host, port, args.reload)
return
run_prod(host, port, skip_build=args.skip_build, reload=args.reload)
if __name__ == "__main__":
main()