Files
2026-03-28 02:39:29 -05:00

246 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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(SCRIPTS_DIR))
import env_check # 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") or shutil.which("npm.cmd")
def _npm(args: list[str], *, cwd: Path, env: dict | None = None) -> int:
full_env = {**os.environ, **(env or {})}
if sys.platform == "win32":
return subprocess.call(
"npm " + " ".join(args),
cwd=cwd,
shell=True,
env=full_env,
)
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:
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
next_args = f"npm run dev -- --hostname 0.0.0.0 --port {next_port}"
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)
if sys.platform == "win32":
ui = subprocess.Popen(next_args, cwd=CONSOLE, shell=True, env=env)
else:
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()
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()