From 1bb73ab3cf7e890fa466f06ffde56f622b31af67 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Thu, 19 Mar 2026 19:53:48 +0800 Subject: [PATCH] feat(cli): add 'webui' group + register command in CLI --- astrbot/cli/__main__.py | 3 +- astrbot/cli/commands/__init__.py | 3 +- astrbot/cli/commands/cmd_webui.py | 168 ++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 astrbot/cli/commands/cmd_webui.py diff --git a/astrbot/cli/__main__.py b/astrbot/cli/__main__.py index 26545d894..2b440ba5b 100644 --- a/astrbot/cli/__main__.py +++ b/astrbot/cli/__main__.py @@ -7,7 +7,7 @@ import click from click.shell_completion import get_completion_class from . import __version__ -from .commands import bk, conf, init, plug, run, uninstall +from .commands import bk, conf, init, plug, run, uninstall, webui logo_tmpl = r""" ___ _______.___________..______ .______ ______ .___________. @@ -77,6 +77,7 @@ def help(command_name: str | None, all: bool) -> None: cli.add_command(init) cli.add_command(run) +cli.add_command(webui) cli.add_command(help) cli.add_command(plug) cli.add_command(conf) diff --git a/astrbot/cli/commands/__init__.py b/astrbot/cli/commands/__init__.py index a3ad038be..acab0b565 100644 --- a/astrbot/cli/commands/__init__.py +++ b/astrbot/cli/commands/__init__.py @@ -4,5 +4,6 @@ from .cmd_init import init from .cmd_plug import plug from .cmd_run import run from .cmd_uninstall import uninstall +from .cmd_webui import webui -__all__ = ["conf", "init", "plug", "run", "uninstall", "bk"] +__all__ = ["bk", "conf", "init", "plug", "run", "uninstall", "webui"] diff --git a/astrbot/cli/commands/cmd_webui.py b/astrbot/cli/commands/cmd_webui.py new file mode 100644 index 000000000..f0b707b42 --- /dev/null +++ b/astrbot/cli/commands/cmd_webui.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +CLI commands to manage the WebUI (dashboard) lifecycle: install/build/dev/serve. + +Commands: +- webui install [--root] : install frontend dependencies (pnpm/npm) +- webui build [--root] : build production assets +- webui dev [--root] : run frontend dev server (long-running) +- webui serve [--root --port] : serve built dist via simple HTTP server + +This file follows the existing AstrBot CLI conventions and raises ClickException +for error conditions so callers and pre-commit hooks can observe failures. +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +import click + +from astrbot.cli.utils.basic import get_astrbot_root +from astrbot.core.utils.astrbot_path import get_astrbot_path + + +def _find_dashboard_source(root: str | None = None) -> Path | None: + """ + Locate the dashboard source directory. + + Priority: + 1. /dashboard + 2. repository/dashboard relative to the project path (get_astrbot_path) + 3. None if not found + """ + base = Path(root) if root else Path(get_astrbot_root()) + cand = base / "dashboard" + if cand.is_dir(): + return cand + + pkg_cand = Path(get_astrbot_path()) / "dashboard" + if pkg_cand.is_dir(): + return pkg_cand + + return None + + +def _find_dashboard_dist(root: str | None = None) -> Path | None: + """ + Locate the built dashboard dist directory. + + Priority: + 1. /data/dist + 2. packaged astrbot/dashboard/dist + 3. None + """ + base = Path(root) if root else Path(get_astrbot_root()) + cand = base / "data" / "dist" + if cand.is_dir(): + return cand + + pkg_dist = Path(get_astrbot_path()) / "dashboard" / "dist" + if pkg_dist.is_dir(): + return pkg_dist + + return None + + +def _choose_package_manager() -> str: + """ + Prefer pnpm, fall back to npm. Raise ClickException if none available. + """ + if shutil.which("pnpm"): + return "pnpm" + if shutil.which("npm"): + return "npm" + raise click.ClickException( + "Neither 'pnpm' nor 'npm' found on PATH. Please install one of them to build/run the dashboard." + ) + + +@click.group(name="webui") +def webui_group() -> None: + """Manage the WebUI (dashboard) frontend: install/build/dev/serve""" + pass + + +@webui_group.command(name="install") +@click.option("--root", type=str, required=False, help="AstrBot root directory") +def webui_install(root: str | None) -> None: + """Install frontend dependencies (pnpm/npm install).""" + src = _find_dashboard_source(root) + if not src: + raise click.ClickException( + "Dashboard source directory not found. Cannot install dependencies." + ) + pm = _choose_package_manager() + click.echo(f"Installing frontend dependencies using {pm} in {src}") + try: + subprocess.run([pm, "install"], cwd=str(src), check=True) + except subprocess.CalledProcessError as e: + raise click.ClickException(f"Failed to install frontend dependencies: {e}") + + +@webui_group.command(name="build") +@click.option("--root", type=str, required=False, help="AstrBot root directory") +def webui_build(root: str | None) -> None: + """Build production dashboard (runs pnpm/npm run build).""" + src = _find_dashboard_source(root) + if not src: + raise click.ClickException( + "Dashboard source directory not found. Cannot build." + ) + pm = _choose_package_manager() + click.echo(f"Building dashboard using {pm} in {src}") + + # Ensure dependencies are installed first (best-effort) + try: + subprocess.run([pm, "install"], cwd=str(src), check=True) + except subprocess.CalledProcessError as e: + raise click.ClickException( + f"Failed to install frontend dependencies before build: {e}" + ) + + try: + subprocess.run([pm, "run", "build"], cwd=str(src), check=True) + except subprocess.CalledProcessError as e: + raise click.ClickException(f"Dashboard build failed: {e}") + + +@webui_group.command(name="dev") +@click.option("--root", type=str, required=False, help="AstrBot root directory") +def webui_dev(root: str | None) -> None: + """Start the frontend dev server (long-running).""" + src = _find_dashboard_source(root) + if not src: + raise click.ClickException( + "Dashboard source directory not found. Cannot start dev server." + ) + pm = _choose_package_manager() + click.echo(f"Starting dashboard dev server using {pm} in {src}") + + # Note: This is intentionally long-running; we do not set check=True to allow + # the process's exit code to be observed by the caller. + try: + subprocess.run([pm, "run", "dev"], cwd=str(src)) + except subprocess.CalledProcessError as e: + raise click.ClickException(f"Dev server exited with error: {e}") + + +@webui_group.command(name="serve") +@click.option("--root", type=str, required=False, help="AstrBot root directory") +@click.option("--port", type=int, required=False, default=8080, help="Port to serve on") +def webui_serve(root: str | None, port: int) -> None: + """Serve built dashboard dist via a simple HTTP server.""" + dist = _find_dashboard_dist(root) + if not dist: + raise click.ClickException( + "Built dashboard dist not found. Run 'astrbot webui build' first." + ) + click.echo(f"Serving dashboard from {dist} on port {port}") + python_bin = shutil.which("python3") or shutil.which("python") or sys.executable + try: + # Long running; let exit code propagate + subprocess.run([python_bin, "-m", "http.server", str(port)], cwd=str(dist)) + except subprocess.CalledProcessError as e: + raise click.ClickException(f"Failed to serve dashboard: {e}")