From 6db0959bb1597e79b041b52137c572e8cfaa03d9 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 16 Mar 2026 23:55:36 +0800 Subject: [PATCH] feat(cli): implement uninstall command and add log-level option - Implement 'astrbot uninstall' to remove systemd service and data files - Add '--log-level' option to 'astrbot run' (default: INFO) - Pass log level config to core logger via env var --- astrbot/cli/__main__.py | 3 +- astrbot/cli/commands/__init__.py | 3 +- astrbot/cli/commands/cmd_init.py | 66 ++++++++++++++++++-- astrbot/cli/commands/cmd_run.py | 24 ++++++-- astrbot/cli/commands/cmd_uninstall.py | 89 +++++++++++++++++++++++++++ astrbot/core/__init__.py | 4 +- scripts/astrbot.service | 5 ++ 7 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 astrbot/cli/commands/cmd_uninstall.py diff --git a/astrbot/cli/__main__.py b/astrbot/cli/__main__.py index 6d48ec28d..29a199341 100644 --- a/astrbot/cli/__main__.py +++ b/astrbot/cli/__main__.py @@ -5,7 +5,7 @@ import sys import click from . import __version__ -from .commands import conf, init, plug, run +from .commands import conf, init, plug, run, uninstall logo_tmpl = r""" ___ _______.___________..______ .______ ______ .___________. @@ -54,6 +54,7 @@ cli.add_command(run) cli.add_command(help) cli.add_command(plug) cli.add_command(conf) +cli.add_command(uninstall) if __name__ == "__main__": cli() diff --git a/astrbot/cli/commands/__init__.py b/astrbot/cli/commands/__init__.py index 1d3e0bca2..8a4410f0f 100644 --- a/astrbot/cli/commands/__init__.py +++ b/astrbot/cli/commands/__init__.py @@ -2,5 +2,6 @@ from .cmd_conf import conf from .cmd_init import init from .cmd_plug import plug from .cmd_run import run +from .cmd_uninstall import uninstall -__all__ = ["conf", "init", "plug", "run"] +__all__ = ["conf", "init", "plug", "run", "uninstall"] diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index 5d1c8e93d..288fdfa32 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -1,4 +1,7 @@ import asyncio +import platform +import shutil +import subprocess from pathlib import Path import click @@ -6,13 +9,36 @@ from filelock import FileLock, Timeout from ..utils import check_dashboard, get_astrbot_root +SYSTEMD_SERVICE = r""" +# user service +[Unit] +Description=AstrBot Service +Documentation=https://github.com/AstrBotDevs/AstrBot +After=network-online.target +Wants=network-online.target -async def initialize_astrbot(astrbot_root: Path) -> None: +[Service] +Type=simple +WorkingDirectory=%h/.local/share/astrbot +ExecStart=/usr/bin/sh -c '/usr/bin/astrbot run || { /usr/bin/astrbot init && /usr/bin/astrbot run; }' +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=astrbot-%u +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=default.target +""" + + +async def initialize_astrbot(astrbot_root: Path, *, yes: bool) -> None: """Execute AstrBot initialization logic""" dot_astrbot = astrbot_root / ".astrbot" if not dot_astrbot.exists(): - if click.confirm( + if yes or click.confirm( f"Install AstrBot to this directory? {astrbot_root}", default=True, abort=True, @@ -29,8 +55,10 @@ async def initialize_astrbot(astrbot_root: Path) -> None: for name, path in paths.items(): path.mkdir(parents=True, exist_ok=True) - click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}") - if click.confirm( + click.echo( + f"{'Created' if not path.exists() else f'{name} Directory exists'}: {path}" + ) + if yes or click.confirm( "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", default=True, ): @@ -40,16 +68,42 @@ async def initialize_astrbot(astrbot_root: Path) -> None: @click.command() -def init() -> None: +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts") +def init(yes: bool) -> None: """Initialize AstrBot""" click.echo("Initializing AstrBot...") + + # 检查当前系统是否为 Linux 且存在 systemd + if platform.system() == "Linux" and shutil.which("systemctl"): + if yes or click.confirm( + "Detected Linux with systemd. Install AstrBot user service?", default=True + ): + user_config_dir = Path.home() / ".config" / "systemd" / "user" + user_config_dir.mkdir(parents=True, exist_ok=True) + + service_path = user_config_dir / "astrbot.service" + + service_path.write_text(SYSTEMD_SERVICE) + click.echo(f"Created service file at {service_path}") + + try: + subprocess.run(["systemctl", "--user", "daemon-reload"], check=True) + click.echo("Systemd daemon reloaded.") + click.echo("Management commands:") + click.echo(" Start: systemctl --user start astrbot") + click.echo(" Stop: systemctl --user stop astrbot") + click.echo(" Enable: systemctl --user enable astrbot") + click.echo(" Log: journalctl --user -u astrbot -f") + except subprocess.CalledProcessError as e: + click.echo(f"Failed to reload systemd daemon: {e}", err=True) + astrbot_root = get_astrbot_root() lock_file = astrbot_root / "astrbot.lock" lock = FileLock(lock_file, timeout=5) try: with lock.acquire(): - asyncio.run(initialize_astrbot(astrbot_root)) + asyncio.run(initialize_astrbot(astrbot_root, yes=yes)) click.echo("Done! You can now run 'astrbot run' to start AstrBot") except Timeout: raise click.ClickException( diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index 98acdcd19..36371ee4a 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -15,7 +15,10 @@ async def run_astrbot(astrbot_root: Path) -> None: from astrbot.core import LogBroker, LogManager, db_helper, logger from astrbot.core.initial_loader import InitialLoader - if os.environ.get("DASHBOARD_ENABLE") == "True": + if ( + os.environ.get("ASTRBOT_DASHBOARD_ENABLE", os.environ.get("DASHBOARD_ENABLE")) + == "True" + ): await check_dashboard(astrbot_root) log_broker = LogBroker() @@ -36,8 +39,15 @@ async def run_astrbot(astrbot_root: Path) -> None: default=False, help="Disable WebUI, run backend only", ) +@click.option( + "--log-level", + help="Log level", + required=False, + type=str, + default="INFO", +) @click.command() -def run(reload: bool, host: str, port: str, backend_only: bool) -> None: +def run(reload: bool, host: str, port: str, backend_only: bool, log_level: str) -> None: """Run AstrBot""" try: os.environ["ASTRBOT_CLI"] = "1" @@ -52,10 +62,14 @@ def run(reload: bool, host: str, port: str, backend_only: bool) -> None: sys.path.insert(0, str(astrbot_root)) if port is not None: - os.environ["DASHBOARD_PORT"] = port + os.environ["ASTRBOT_DASHBOARD_PORT"] = port + os.environ["DASHBOARD_PORT"] = port # 今后应该移除 if host is not None: - os.environ["DASHBOARD_HOST"] = host - os.environ["DASHBOARD_ENABLE"] = str(not backend_only) + os.environ["ASTRBOT_DASHBOARD_HOST"] = host + os.environ["DASHBOARD_HOST"] = host # 今后应该移除 + os.environ["ASTRBOT_DASHBOARD_ENABLE"] = str(not backend_only) + os.environ["DASHBOARD_ENABLE"] = str(not backend_only) # 今后应该移除 + os.environ["ASTRBOT_LOG_LEVEL"] = log_level if reload: click.echo("Plugin auto-reload enabled") diff --git a/astrbot/cli/commands/cmd_uninstall.py b/astrbot/cli/commands/cmd_uninstall.py new file mode 100644 index 000000000..0b7481a47 --- /dev/null +++ b/astrbot/cli/commands/cmd_uninstall.py @@ -0,0 +1,89 @@ +import platform +import shutil +import subprocess +from pathlib import Path + +import click + +from ..utils import get_astrbot_root + + +@click.command() +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts") +@click.option( + "--keep-data", is_flag=True, help="Keep data directory (config, plugins, etc.)" +) +def uninstall(yes: bool, keep_data: bool) -> None: + """Uninstall AstrBot systemd service and cleanup data""" + + # 1. Remove Systemd Service + if platform.system() == "Linux" and shutil.which("systemctl"): + service_path = Path.home() / ".config" / "systemd" / "user" / "astrbot.service" + + if service_path.exists(): + if yes or click.confirm( + "Detected AstrBot systemd service. Stop and remove it?", + default=True, + ): + try: + click.echo("Stopping AstrBot service...") + subprocess.run( + ["systemctl", "--user", "stop", "astrbot"], check=False + ) + + click.echo("Disabling AstrBot service...") + subprocess.run( + ["systemctl", "--user", "disable", "astrbot"], check=False + ) + + click.echo(f"Removing service file: {service_path}") + service_path.unlink() + + click.echo("Reloading systemd daemon...") + subprocess.run(["systemctl", "--user", "daemon-reload"], check=True) + click.echo("Systemd service uninstalled.") + + except subprocess.CalledProcessError as e: + click.echo(f"Failed to remove systemd service: {e}", err=True) + except Exception as e: + click.echo( + f"An error occurred during service removal: {e}", err=True + ) + + # 2. Remove Data + astrbot_root = get_astrbot_root() + data_dir = astrbot_root / "data" + dot_astrbot = astrbot_root / ".astrbot" + lock_file = astrbot_root / "astrbot.lock" + + if keep_data: + click.echo("Skipping data removal as requested.") + return + + # Check if this looks like an AstrBot root before blowing things up + if not dot_astrbot.exists() and not data_dir.exists(): + click.echo("No AstrBot initialization found in current directory.") + return + + if yes or click.confirm( + f"Are you sure you want to remove AstrBot data at {astrbot_root}? \n" + f"This will delete:\n" + f" - {data_dir} (Config, Plugins, Database)\n" + f" - {dot_astrbot}\n" + f" - {lock_file}", + default=False, + abort=True, + ): + if data_dir.exists(): + click.echo(f"Removing directory: {data_dir}") + shutil.rmtree(data_dir) + + if dot_astrbot.exists(): + click.echo(f"Removing file: {dot_astrbot}") + dot_astrbot.unlink() + + if lock_file.exists(): + click.echo(f"Removing file: {lock_file}") + lock_file.unlink() + + click.echo("AstrBot data removed successfully.") diff --git a/astrbot/core/__init__.py b/astrbot/core/__init__.py index 51690ede2..4bcc28c7a 100644 --- a/astrbot/core/__init__.py +++ b/astrbot/core/__init__.py @@ -34,7 +34,9 @@ astrbot_config = AstrBotConfig() t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img") html_renderer = HtmlRenderer(t2i_base_url) logger = LogManager.GetLogger(log_name="astrbot") -LogManager.configure_logger(logger, astrbot_config) +LogManager.configure_logger( + logger, astrbot_config, override_level=os.getenv("ASTRBOT_LOG_LEVEL") +) LogManager.configure_trace_logger(astrbot_config) db_helper = SQLiteDatabase(DB_PATH) # 简单的偏好设置存储, 这里后续应该存储到数据库中, 一些部分可以存储到配置中 diff --git a/scripts/astrbot.service b/scripts/astrbot.service index fdf891be9..b99a0d946 100644 --- a/scripts/astrbot.service +++ b/scripts/astrbot.service @@ -1,5 +1,7 @@ +# user service [Unit] Description=AstrBot Service +Documentation=https://github.com/AstrBotDevs/AstrBot After=network-online.target Wants=network-online.target @@ -9,6 +11,9 @@ WorkingDirectory=%h/.local/share/astrbot ExecStart=/usr/bin/sh -c '/usr/bin/astrbot run || { /usr/bin/astrbot init && /usr/bin/astrbot run; }' Restart=on-failure RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=astrbot-%u Environment=PYTHONUNBUFFERED=1 [Install]