Skip to content

Commit 6db0959

Browse files
committed
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
1 parent a05bfed commit 6db0959

File tree

7 files changed

+180
-14
lines changed

7 files changed

+180
-14
lines changed

astrbot/cli/__main__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import click
66

77
from . import __version__
8-
from .commands import conf, init, plug, run
8+
from .commands import conf, init, plug, run, uninstall
99

1010
logo_tmpl = r"""
1111
___ _______.___________..______ .______ ______ .___________.
@@ -54,6 +54,7 @@ def help(command_name: str | None) -> None:
5454
cli.add_command(help)
5555
cli.add_command(plug)
5656
cli.add_command(conf)
57+
cli.add_command(uninstall)
5758

5859
if __name__ == "__main__":
5960
cli()

astrbot/cli/commands/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
from .cmd_init import init
33
from .cmd_plug import plug
44
from .cmd_run import run
5+
from .cmd_uninstall import uninstall
56

6-
__all__ = ["conf", "init", "plug", "run"]
7+
__all__ = ["conf", "init", "plug", "run", "uninstall"]

astrbot/cli/commands/cmd_init.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,44 @@
11
import asyncio
2+
import platform
3+
import shutil
4+
import subprocess
25
from pathlib import Path
36

47
import click
58
from filelock import FileLock, Timeout
69

710
from ..utils import check_dashboard, get_astrbot_root
811

12+
SYSTEMD_SERVICE = r"""
13+
# user service
14+
[Unit]
15+
Description=AstrBot Service
16+
Documentation=https://github.com/AstrBotDevs/AstrBot
17+
After=network-online.target
18+
Wants=network-online.target
919
10-
async def initialize_astrbot(astrbot_root: Path) -> None:
20+
[Service]
21+
Type=simple
22+
WorkingDirectory=%h/.local/share/astrbot
23+
ExecStart=/usr/bin/sh -c '/usr/bin/astrbot run || { /usr/bin/astrbot init && /usr/bin/astrbot run; }'
24+
Restart=on-failure
25+
RestartSec=5
26+
StandardOutput=journal
27+
StandardError=journal
28+
SyslogIdentifier=astrbot-%u
29+
Environment=PYTHONUNBUFFERED=1
30+
31+
[Install]
32+
WantedBy=default.target
33+
"""
34+
35+
36+
async def initialize_astrbot(astrbot_root: Path, *, yes: bool) -> None:
1137
"""Execute AstrBot initialization logic"""
1238
dot_astrbot = astrbot_root / ".astrbot"
1339

1440
if not dot_astrbot.exists():
15-
if click.confirm(
41+
if yes or click.confirm(
1642
f"Install AstrBot to this directory? {astrbot_root}",
1743
default=True,
1844
abort=True,
@@ -29,8 +55,10 @@ async def initialize_astrbot(astrbot_root: Path) -> None:
2955

3056
for name, path in paths.items():
3157
path.mkdir(parents=True, exist_ok=True)
32-
click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}")
33-
if click.confirm(
58+
click.echo(
59+
f"{'Created' if not path.exists() else f'{name} Directory exists'}: {path}"
60+
)
61+
if yes or click.confirm(
3462
"是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)",
3563
default=True,
3664
):
@@ -40,16 +68,42 @@ async def initialize_astrbot(astrbot_root: Path) -> None:
4068

4169

4270
@click.command()
43-
def init() -> None:
71+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
72+
def init(yes: bool) -> None:
4473
"""Initialize AstrBot"""
4574
click.echo("Initializing AstrBot...")
75+
76+
# 检查当前系统是否为 Linux 且存在 systemd
77+
if platform.system() == "Linux" and shutil.which("systemctl"):
78+
if yes or click.confirm(
79+
"Detected Linux with systemd. Install AstrBot user service?", default=True
80+
):
81+
user_config_dir = Path.home() / ".config" / "systemd" / "user"
82+
user_config_dir.mkdir(parents=True, exist_ok=True)
83+
84+
service_path = user_config_dir / "astrbot.service"
85+
86+
service_path.write_text(SYSTEMD_SERVICE)
87+
click.echo(f"Created service file at {service_path}")
88+
89+
try:
90+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
91+
click.echo("Systemd daemon reloaded.")
92+
click.echo("Management commands:")
93+
click.echo(" Start: systemctl --user start astrbot")
94+
click.echo(" Stop: systemctl --user stop astrbot")
95+
click.echo(" Enable: systemctl --user enable astrbot")
96+
click.echo(" Log: journalctl --user -u astrbot -f")
97+
except subprocess.CalledProcessError as e:
98+
click.echo(f"Failed to reload systemd daemon: {e}", err=True)
99+
46100
astrbot_root = get_astrbot_root()
47101
lock_file = astrbot_root / "astrbot.lock"
48102
lock = FileLock(lock_file, timeout=5)
49103

50104
try:
51105
with lock.acquire():
52-
asyncio.run(initialize_astrbot(astrbot_root))
106+
asyncio.run(initialize_astrbot(astrbot_root, yes=yes))
53107
click.echo("Done! You can now run 'astrbot run' to start AstrBot")
54108
except Timeout:
55109
raise click.ClickException(

astrbot/cli/commands/cmd_run.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ async def run_astrbot(astrbot_root: Path) -> None:
1515
from astrbot.core import LogBroker, LogManager, db_helper, logger
1616
from astrbot.core.initial_loader import InitialLoader
1717

18-
if os.environ.get("DASHBOARD_ENABLE") == "True":
18+
if (
19+
os.environ.get("ASTRBOT_DASHBOARD_ENABLE", os.environ.get("DASHBOARD_ENABLE"))
20+
== "True"
21+
):
1922
await check_dashboard(astrbot_root)
2023

2124
log_broker = LogBroker()
@@ -36,8 +39,15 @@ async def run_astrbot(astrbot_root: Path) -> None:
3639
default=False,
3740
help="Disable WebUI, run backend only",
3841
)
42+
@click.option(
43+
"--log-level",
44+
help="Log level",
45+
required=False,
46+
type=str,
47+
default="INFO",
48+
)
3949
@click.command()
40-
def run(reload: bool, host: str, port: str, backend_only: bool) -> None:
50+
def run(reload: bool, host: str, port: str, backend_only: bool, log_level: str) -> None:
4151
"""Run AstrBot"""
4252
try:
4353
os.environ["ASTRBOT_CLI"] = "1"
@@ -52,10 +62,14 @@ def run(reload: bool, host: str, port: str, backend_only: bool) -> None:
5262
sys.path.insert(0, str(astrbot_root))
5363

5464
if port is not None:
55-
os.environ["DASHBOARD_PORT"] = port
65+
os.environ["ASTRBOT_DASHBOARD_PORT"] = port
66+
os.environ["DASHBOARD_PORT"] = port # 今后应该移除
5667
if host is not None:
57-
os.environ["DASHBOARD_HOST"] = host
58-
os.environ["DASHBOARD_ENABLE"] = str(not backend_only)
68+
os.environ["ASTRBOT_DASHBOARD_HOST"] = host
69+
os.environ["DASHBOARD_HOST"] = host # 今后应该移除
70+
os.environ["ASTRBOT_DASHBOARD_ENABLE"] = str(not backend_only)
71+
os.environ["DASHBOARD_ENABLE"] = str(not backend_only) # 今后应该移除
72+
os.environ["ASTRBOT_LOG_LEVEL"] = log_level
5973

6074
if reload:
6175
click.echo("Plugin auto-reload enabled")
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import platform
2+
import shutil
3+
import subprocess
4+
from pathlib import Path
5+
6+
import click
7+
8+
from ..utils import get_astrbot_root
9+
10+
11+
@click.command()
12+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
13+
@click.option(
14+
"--keep-data", is_flag=True, help="Keep data directory (config, plugins, etc.)"
15+
)
16+
def uninstall(yes: bool, keep_data: bool) -> None:
17+
"""Uninstall AstrBot systemd service and cleanup data"""
18+
19+
# 1. Remove Systemd Service
20+
if platform.system() == "Linux" and shutil.which("systemctl"):
21+
service_path = Path.home() / ".config" / "systemd" / "user" / "astrbot.service"
22+
23+
if service_path.exists():
24+
if yes or click.confirm(
25+
"Detected AstrBot systemd service. Stop and remove it?",
26+
default=True,
27+
):
28+
try:
29+
click.echo("Stopping AstrBot service...")
30+
subprocess.run(
31+
["systemctl", "--user", "stop", "astrbot"], check=False
32+
)
33+
34+
click.echo("Disabling AstrBot service...")
35+
subprocess.run(
36+
["systemctl", "--user", "disable", "astrbot"], check=False
37+
)
38+
39+
click.echo(f"Removing service file: {service_path}")
40+
service_path.unlink()
41+
42+
click.echo("Reloading systemd daemon...")
43+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
44+
click.echo("Systemd service uninstalled.")
45+
46+
except subprocess.CalledProcessError as e:
47+
click.echo(f"Failed to remove systemd service: {e}", err=True)
48+
except Exception as e:
49+
click.echo(
50+
f"An error occurred during service removal: {e}", err=True
51+
)
52+
53+
# 2. Remove Data
54+
astrbot_root = get_astrbot_root()
55+
data_dir = astrbot_root / "data"
56+
dot_astrbot = astrbot_root / ".astrbot"
57+
lock_file = astrbot_root / "astrbot.lock"
58+
59+
if keep_data:
60+
click.echo("Skipping data removal as requested.")
61+
return
62+
63+
# Check if this looks like an AstrBot root before blowing things up
64+
if not dot_astrbot.exists() and not data_dir.exists():
65+
click.echo("No AstrBot initialization found in current directory.")
66+
return
67+
68+
if yes or click.confirm(
69+
f"Are you sure you want to remove AstrBot data at {astrbot_root}? \n"
70+
f"This will delete:\n"
71+
f" - {data_dir} (Config, Plugins, Database)\n"
72+
f" - {dot_astrbot}\n"
73+
f" - {lock_file}",
74+
default=False,
75+
abort=True,
76+
):
77+
if data_dir.exists():
78+
click.echo(f"Removing directory: {data_dir}")
79+
shutil.rmtree(data_dir)
80+
81+
if dot_astrbot.exists():
82+
click.echo(f"Removing file: {dot_astrbot}")
83+
dot_astrbot.unlink()
84+
85+
if lock_file.exists():
86+
click.echo(f"Removing file: {lock_file}")
87+
lock_file.unlink()
88+
89+
click.echo("AstrBot data removed successfully.")

astrbot/core/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img")
3535
html_renderer = HtmlRenderer(t2i_base_url)
3636
logger = LogManager.GetLogger(log_name="astrbot")
37-
LogManager.configure_logger(logger, astrbot_config)
37+
LogManager.configure_logger(
38+
logger, astrbot_config, override_level=os.getenv("ASTRBOT_LOG_LEVEL")
39+
)
3840
LogManager.configure_trace_logger(astrbot_config)
3941
db_helper = SQLiteDatabase(DB_PATH)
4042
# 简单的偏好设置存储, 这里后续应该存储到数据库中, 一些部分可以存储到配置中

scripts/astrbot.service

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
# user service
12
[Unit]
23
Description=AstrBot Service
4+
Documentation=https://github.com/AstrBotDevs/AstrBot
35
After=network-online.target
46
Wants=network-online.target
57

@@ -9,6 +11,9 @@ WorkingDirectory=%h/.local/share/astrbot
911
ExecStart=/usr/bin/sh -c '/usr/bin/astrbot run || { /usr/bin/astrbot init && /usr/bin/astrbot run; }'
1012
Restart=on-failure
1113
RestartSec=5
14+
StandardOutput=journal
15+
StandardError=journal
16+
SyslogIdentifier=astrbot-%u
1217
Environment=PYTHONUNBUFFERED=1
1318

1419
[Install]

0 commit comments

Comments
 (0)