Ctrl K
ComunidadGuías…

scripts/start_webui.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Windows-friendly WebUI launcher.

Keep web_ui.bat small and put checks here, where quoting and control flow are
less fragile than in cmd.exe batch syntax.
"""

from __future__ import annotations

import json
import shutil
import subprocess
import sys
from pathlib import Path
import re

import ensure_runtime_deps


ROOT = Path(__file__).resolve().parents[1]
FRONTEND_DIR = ROOT / "frontend-v2"
STATIC_DIR = ROOT / "static-v2"
ASSET_REF_RE = re.compile(r"""(?:src|href)=["'](/v2-assets/[^"']+)["']""")


def _frontend_source_mtime() -> float:
    candidates = [
        FRONTEND_DIR / "package.json",
        FRONTEND_DIR / "package-lock.json",
        FRONTEND_DIR / "vite.config.ts",
        FRONTEND_DIR / "tsconfig.json",
    ]
    candidates.extend(
        path for root_name in ("src", "public")
        for path in (FRONTEND_DIR / root_name).rglob("*")
        if path.is_file()
    )
    return max((path.stat().st_mtime for path in candidates if path.is_file()), default=0.0)


def frontend_built() -> bool:
    assets_dir = STATIC_DIR / "assets"
    index_path = STATIC_DIR / "index.html"
    if not index_path.exists() or not assets_dir.is_dir() or not any(assets_dir.glob("*.js")):
        return False
    html = index_path.read_text(encoding="utf-8", errors="ignore")
    for ref in ASSET_REF_RE.findall(html):
        relative = ref.removeprefix("/v2-assets/")
        if not (STATIC_DIR / relative).exists():
            return False
    if _frontend_source_mtime() > index_path.stat().st_mtime:
        return False
    return True


def run(cmd: list[str], cwd: Path) -> int:
    print("> " + " ".join(cmd), flush=True)
    return subprocess.run(cmd, cwd=str(cwd)).returncode


def frontend_dependencies_ready() -> bool:
    """Return whether all package.json dependencies and build tools are installed."""
    node_modules = FRONTEND_DIR / "node_modules"
    bin_dir = node_modules / ".bin"
    if not all(
        any((bin_dir / name).is_file() for name in (tool, f"{tool}.cmd", f"{tool}.ps1"))
        for tool in ("vue-tsc", "vite")
    ):
        return False
    try:
        package = json.loads((FRONTEND_DIR / "package.json").read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return False
    dependencies = {
        *package.get("dependencies", {}),
        *package.get("devDependencies", {}),
    }
    return all((node_modules / name / "package.json").is_file() for name in dependencies)


def ensure_frontend() -> int:
    if frontend_built():
        return 0
    print("\n未检测到前端构建产物,正在构建 frontend-v2...", flush=True)
    npm = shutil.which("npm.cmd") or shutil.which("npm")
    if not npm:
        print(
            "未找到 npm。请先安装 Node.js 20.19+ 或 22.12+,然后重新运行 web_ui.bat。",
            file=sys.stderr,
            flush=True,
        )
        return 1
    if not frontend_dependencies_ready():
        code = run([npm, "ci", "--include=dev"], FRONTEND_DIR)
        if code:
            return code
    return run([npm, "run", "build"], FRONTEND_DIR)


def main() -> int:
    code = ensure_runtime_deps.main()
    if code:
        return code
    code = ensure_frontend()
    if code:
        return code
    return subprocess.run([sys.executable, str(ROOT / "web_server.py")], cwd=str(ROOT)).returncode


if __name__ == "__main__":
    raise SystemExit(main())