From 12cdf454b45bb8efdaaa6913f5e9f14831a39809 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Wed, 18 Mar 2026 13:52:00 +0800 Subject: [PATCH] feat(cli): enhance run/init/uninstall for systemd & debug; refactor(env): standardize vars --- .env.example | 93 +++++++++++++++++++ AGENTS.md | 6 ++ astrbot/cli/commands/cmd_init.py | 10 ++- astrbot/cli/commands/cmd_run.py | 124 +++++++++++++++++++++++++- astrbot/cli/commands/cmd_uninstall.py | 17 +++- astrbot/core/utils/webhook_utils.py | 4 +- astrbot/dashboard/server.py | 29 +++--- 7 files changed, 262 insertions(+), 21 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..8f27620d3 --- /dev/null +++ b/.env.example @@ -0,0 +1,93 @@ +# ========================================== +# AstrBot Environment Configuration Example +# ========================================== +# Copy this file to .env and adjust the values as needed. +# Note: Variables set here will override default configurations. + +# ------------------------------------------ +# Core Configuration (核心配置) +# ------------------------------------------ + +# AstrBot root directory path. Defaults to current working directory or ~/.astrbot for desktop client. +# ASTRBOT_ROOT=/path/to/astrbot + +# Log level. Options: DEBUG, INFO, WARNING, ERROR, CRITICAL. Default: INFO. +# ASTRBOT_LOG_LEVEL=INFO + +# Enable plugin auto-reload. Set to "1" to enable. Useful for development. +# ASTRBOT_RELOAD=0 + +# Disable metrics upload. Set to "1" to disable anonymous usage statistics. +# ASTRBOT_DISABLE_METRICS=0 + +# Python executable path override (used for local code execution feature). +# PYTHON=/usr/bin/python3 + +# Enable demo mode (might restrict some features). +# DEMO_MODE=False + +# Enable testing mode (affects logging and some behaviors). +# TESTING=False + +# Flag indicating execution via desktop client (Internal use mostly). +# ASTRBOT_DESKTOP_CLIENT=0 + +# Flag indicating execution via systemd service. +# ASTRBOT_SYSTEMD=0 + +# ------------------------------------------ +# Dashboard Configuration (管理面板配置) +# ------------------------------------------ + +# Enable or disable the WebUI Dashboard. Default: True. +# ASTRBOT_DASHBOARD_ENABLE=True + +# Dashboard bind host. Default: 0.0.0.0 (listen on all interfaces). +# ASTRBOT_DASHBOARD_HOST=0.0.0.0 + +# Dashboard bind port. Default: 6185. +# ASTRBOT_DASHBOARD_PORT=6185 + +# Enable SSL (HTTPS) for the dashboard. +# ASTRBOT_DASHBOARD_SSL_ENABLE=False + +# SSL Certificate path (required if SSL is enabled). +# ASTRBOT_DASHBOARD_SSL_CERT=/path/to/cert.pem + +# SSL Key path (required if SSL is enabled). +# ASTRBOT_DASHBOARD_SSL_KEY=/path/to/key.pem + +# SSL CA Certificates path (optional). +# ASTRBOT_DASHBOARD_SSL_CA_CERTS=/path/to/ca.pem + +# ------------------------------------------ +# Network Configuration (网络配置) +# ------------------------------------------ + +# HTTP/HTTPS Proxy URL (e.g., http://127.0.0.1:7890). +# http_proxy= +# https_proxy= + +# No proxy list (comma-separated domains/IPs to bypass proxy). +# no_proxy=localhost,127.0.0.1 + +# ------------------------------------------ +# Integrations (第三方集成) +# ------------------------------------------ + +# Alibaba DashScope API Key (used for Rerank service). +# DASHSCOPE_API_KEY=sk-xxxxxxxxxxxx + +# Coze Integration +# COZE_API_KEY= +# COZE_BOT_ID= + +# Computer Use data directory (for screenshot/file storage related to computer control). +# BAY_DATA_DIR= + +# ------------------------------------------ +# Platform Specific (平台特定配置) +# ------------------------------------------ + +# Test mode for QQ Official Bot. +# TEST_MODE=off diff --git a/AGENTS.md b/AGENTS.md index 4f1d62950..4ce4f5240 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,12 @@ Runs on `http://localhost:3000` by default. 5. Use English for all new comments. 6. For path handling, use `pathlib.Path` instead of string paths, and use `astrbot.core.utils.path_utils` to get the AstrBot data and temp directory. 7. Use Python 3.12+ type hinting syntax (e.g., `list[str]` over `List[str]`, `int | None` over `Optional[int]`). Avoid using `Any` and ensure comprehensive type annotations are provided. +8. When introducing new environment variables: + - Use the `ASTRBOT_` prefix for naming (e.g., `ASTRBOT_ENABLE_FEATURE`). + - Add the variable and description to `.env.example`. + - Update `astrbot/cli/commands/cmd_run.py`: + - Add to the module docstring under "Environment Variables Used in Project". + - Add to the `keys_to_print` list in the `run` function for debug output. ## PR instructions diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index b60e109f0..649a1dd66 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -1,4 +1,5 @@ import asyncio +import os from pathlib import Path import click @@ -44,7 +45,11 @@ async def initialize_astrbot( default=True, ) ): - await check_dashboard(astrbot_root) + # 避免在 systemd 模式下因等待输入而阻塞 + if os.environ.get("ASTRBOT_SYSTEMD") == "1": + click.echo("Systemd detected: Skipping dashboard check.") + else: + await check_dashboard(astrbot_root) else: click.echo("你可以使用在线面版(v4.14.4+),填写后端地址的方式来控制。") @@ -56,6 +61,9 @@ def init(yes: bool, backend_only: bool) -> None: """Initialize AstrBot""" click.echo("Initializing AstrBot...") + if os.environ.get("ASTRBOT_SYSTEMD") == "1": + yes = True + astrbot_root = astrbot_paths.root lock_file = astrbot_root / "astrbot.lock" lock = FileLock(lock_file, timeout=5) diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index 8a52b81e4..64cff60f4 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -1,3 +1,40 @@ +"""AstrBot Run +Environment Variables Used in Project: + +Core: +- `ASTRBOT_ROOT`: AstrBot root directory path. +- `ASTRBOT_LOG_LEVEL`: Log level (e.g. INFO, DEBUG). +- `ASTRBOT_CLI`: Flag indicating execution via CLI. +- `ASTRBOT_DESKTOP_CLIENT`: Flag indicating execution via desktop client. +- `ASTRBOT_SYSTEMD`: Flag indicating execution via systemd service. +- `ASTRBOT_RELOAD`: Enable plugin auto-reload (set to "1"). +- `ASTRBOT_DISABLE_METRICS`: Disable metrics upload (set to "1"). +- `TESTING`: Enable testing mode. +- `DEMO_MODE`: Enable demo mode. +- `PYTHON`: Python executable path override (for local code execution). + +Dashboard: +- `ASTRBOT_DASHBOARD_ENABLE` / `DASHBOARD_ENABLE`: Enable/Disable Dashboard. +- `ASTRBOT_DASHBOARD_HOST` / `DASHBOARD_HOST`: Dashboard bind host. +- `ASTRBOT_DASHBOARD_PORT` / `DASHBOARD_PORT`: Dashboard bind port. +- `ASTRBOT_DASHBOARD_SSL_ENABLE` / `DASHBOARD_SSL_ENABLE`: Enable SSL. +- `ASTRBOT_DASHBOARD_SSL_CERT` / `DASHBOARD_SSL_CERT`: SSL Certificate path. +- `ASTRBOT_DASHBOARD_SSL_KEY` / `DASHBOARD_SSL_KEY`: SSL Key path. +- `ASTRBOT_DASHBOARD_SSL_CA_CERTS` / `DASHBOARD_SSL_CA_CERTS`: SSL CA Certs path. + +Network: +- `http_proxy` / `https_proxy`: Proxy URL. +- `no_proxy`: No proxy list. + +Integrations: +- `DASHSCOPE_API_KEY`: Alibaba DashScope API Key (for Rerank). +- `COZE_API_KEY` / `COZE_BOT_ID`: Coze integration. +- `BAY_DATA_DIR`: Computer Use data directory. + +Platform Specific: +- `TEST_MODE`: Test mode for QQOfficial. +""" + import asyncio import os import sys @@ -21,7 +58,9 @@ async def run_astrbot(astrbot_root: Path) -> None: os.environ.get("ASTRBOT_DASHBOARD_ENABLE", os.environ.get("DASHBOARD_ENABLE")) == "True" ): - await check_dashboard(astrbot_root) + # 避免在 systemd 模式下因等待输入而阻塞 + if os.environ.get("ASTRBOT_SYSTEMD") != "1": + await check_dashboard(astrbot_root) log_broker = LogBroker() LogManager.set_queue_handler(logger, log_broker) @@ -35,6 +74,7 @@ async def run_astrbot(astrbot_root: Path) -> None: @click.option("--reload", "-r", is_flag=True, help="Auto-reload plugins") @click.option("--host", "-H", help="AstrBot Dashboard Host", required=False, type=str) @click.option("--port", "-p", help="AstrBot Dashboard port", required=False, type=str) +@click.option("--root", help="AstrBot root directory", required=False, type=str) @click.option( "--backend-only", is_flag=True, @@ -48,12 +88,43 @@ async def run_astrbot(astrbot_root: Path) -> None: type=str, default="INFO", ) +@click.option("--debug", is_flag=True, help="Enable debug mode") @click.command() -def run(reload: bool, host: str, port: str, backend_only: bool, log_level: str) -> None: +def run( + reload: bool, + host: str, + port: str, + root: str, + backend_only: bool, + log_level: str, + debug: bool, +) -> None: """Run AstrBot""" try: + if debug: + log_level = "DEBUG" + + # Normalize environment variables for backward compatibility + # If the legacy env var is set but the new one isn't, copy it over. + env_map = { + "DASHBOARD_ENABLE": "ASTRBOT_DASHBOARD_ENABLE", + "DASHBOARD_HOST": "ASTRBOT_DASHBOARD_HOST", + "DASHBOARD_PORT": "ASTRBOT_DASHBOARD_PORT", + "DASHBOARD_SSL_ENABLE": "ASTRBOT_DASHBOARD_SSL_ENABLE", + "DASHBOARD_SSL_CERT": "ASTRBOT_DASHBOARD_SSL_CERT", + "DASHBOARD_SSL_KEY": "ASTRBOT_DASHBOARD_SSL_KEY", + "DASHBOARD_SSL_CA_CERTS": "ASTRBOT_DASHBOARD_SSL_CA_CERTS", + } + for legacy, new in env_map.items(): + if legacy in os.environ and new not in os.environ: + os.environ[new] = os.environ[legacy] + os.environ["ASTRBOT_CLI"] = "1" - astrbot_root = astrbot_paths.root + if root: + os.environ["ASTRBOT_ROOT"] = root + astrbot_root = Path(root) + else: + astrbot_root = astrbot_paths.root if not check_astrbot_root(astrbot_root): raise click.ClickException( @@ -77,6 +148,53 @@ def run(reload: bool, host: str, port: str, backend_only: bool, log_level: str) click.echo("Plugin auto-reload enabled") os.environ["ASTRBOT_RELOAD"] = "1" + if debug: + keys_to_print = [ + "ASTRBOT_ROOT", + "ASTRBOT_LOG_LEVEL", + "ASTRBOT_CLI", + "ASTRBOT_DESKTOP_CLIENT", + "ASTRBOT_SYSTEMD", + "ASTRBOT_RELOAD", + "ASTRBOT_DISABLE_METRICS", + "TESTING", + "DEMO_MODE", + "PYTHON", + "ASTRBOT_DASHBOARD_ENABLE", + "DASHBOARD_ENABLE", + "ASTRBOT_DASHBOARD_HOST", + "DASHBOARD_HOST", + "ASTRBOT_DASHBOARD_PORT", + "DASHBOARD_PORT", + "ASTRBOT_DASHBOARD_SSL_ENABLE", + "DASHBOARD_SSL_ENABLE", + "ASTRBOT_DASHBOARD_SSL_CERT", + "DASHBOARD_SSL_CERT", + "ASTRBOT_DASHBOARD_SSL_KEY", + "DASHBOARD_SSL_KEY", + "ASTRBOT_DASHBOARD_SSL_CA_CERTS", + "DASHBOARD_SSL_CA_CERTS", + "http_proxy", + "https_proxy", + "no_proxy", + "DASHSCOPE_API_KEY", + "COZE_API_KEY", + "COZE_BOT_ID", + "BAY_DATA_DIR", + "TEST_MODE", + ] + click.secho("\n[Debug Mode] Environment Variables:", fg="yellow", bold=True) + for key in keys_to_print: + if key in os.environ: + val = os.environ[key] + if "KEY" in key or "PASSWORD" in key or "SECRET" in key: + if len(val) > 8: + val = val[:4] + "****" + val[-4:] + else: + val = "****" + click.echo(f" {click.style(key, fg='cyan')}: {val}") + click.echo("") + lock_file = astrbot_root / "astrbot.lock" lock = FileLock(lock_file, timeout=5) with lock.acquire(): diff --git a/astrbot/cli/commands/cmd_uninstall.py b/astrbot/cli/commands/cmd_uninstall.py index ead7a6ce1..cea900313 100644 --- a/astrbot/cli/commands/cmd_uninstall.py +++ b/astrbot/cli/commands/cmd_uninstall.py @@ -1,3 +1,4 @@ +import os import platform import shutil import subprocess @@ -16,6 +17,9 @@ from astrbot.core.utils.astrbot_path import astrbot_paths def uninstall(yes: bool, keep_data: bool) -> None: """Uninstall AstrBot systemd service and cleanup data""" + if os.environ.get("ASTRBOT_SYSTEMD") == "1": + yes = True + # 1. Remove Systemd Service if platform.system() == "Linux" and shutil.which("systemctl"): service_path = Path.home() / ".config" / "systemd" / "user" / "astrbot.service" @@ -26,10 +30,15 @@ def uninstall(yes: bool, keep_data: bool) -> None: default=True, ): try: - click.echo("Stopping AstrBot service...") - subprocess.run( - ["systemctl", "--user", "stop", "astrbot"], check=False - ) + if os.environ.get("ASTRBOT_SYSTEMD") != "1": + click.echo("Stopping AstrBot service...") + subprocess.run( + ["systemctl", "--user", "stop", "astrbot"], check=False + ) + else: + click.echo( + "Skipping stop service (running as systemd service)." + ) click.echo("Disabling AstrBot service...") subprocess.run( diff --git a/astrbot/core/utils/webhook_utils.py b/astrbot/core/utils/webhook_utils.py index 40dada3cb..c4dfc9c6f 100644 --- a/astrbot/core/utils/webhook_utils.py +++ b/astrbot/core/utils/webhook_utils.py @@ -22,8 +22,8 @@ def _get_dashboard_port() -> int: def _is_dashboard_ssl_enabled() -> bool: - env_ssl = os.environ.get("DASHBOARD_SSL_ENABLE") or os.environ.get( - "ASTRBOT_DASHBOARD_SSL_ENABLE" + env_ssl = os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE") or os.environ.get( + "DASHBOARD_SSL_ENABLE" ) if env_ssl is not None: return env_ssl.strip().lower() in {"1", "true", "yes", "on"} diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 08ff7c483..8e0868ad1 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -90,7 +90,9 @@ class AstrBotDashboard: def _check_webui_enabled(self) -> bool: cfg = self.config.get("dashboard", {}) - _env = os.environ.get("DASHBOARD_ENABLE") + _env = os.environ.get( + "ASTRBOT_DASHBOARD_ENABLE", os.environ.get("DASHBOARD_ENABLE") + ) if _env is not None: return _env.lower() in ("true", "1", "yes") return cfg.get("enable", True) @@ -384,15 +386,20 @@ class AstrBotDashboard: ) dashboard_config = self.config.get("dashboard", {}) - host = os.environ.get("DASHBOARD_HOST") or dashboard_config.get( - "host", "0.0.0.0" + host = ( + os.environ.get("ASTRBOT_DASHBOARD_HOST") + or os.environ.get("DASHBOARD_HOST") + or dashboard_config.get("host", "0.0.0.0") ) port = int( - os.environ.get("DASHBOARD_PORT") or dashboard_config.get("port", 6185) + os.environ.get("ASTRBOT_DASHBOARD_PORT") + or os.environ.get("DASHBOARD_PORT") + or dashboard_config.get("port", 6185) ) ssl_config = dashboard_config.get("ssl", {}) ssl_enable = _parse_env_bool( - os.environ.get("DASHBOARD_SSL_ENABLE"), + os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE") + or os.environ.get("DASHBOARD_SSL_ENABLE"), ssl_config.get("enable", False), ) @@ -434,18 +441,18 @@ class AstrBotDashboard: if ssl_enable: cert_file = ( - os.environ.get("DASHBOARD_SSL_CERT") - or os.environ.get("ASTRBOT_DASHBOARD_SSL_CERT") + os.environ.get("ASTRBOT_DASHBOARD_SSL_CERT") + or os.environ.get("DASHBOARD_SSL_CERT") or ssl_config.get("cert_file", "") ) key_file = ( - os.environ.get("DASHBOARD_SSL_KEY") - or os.environ.get("ASTRBOT_DASHBOARD_SSL_KEY") + os.environ.get("ASTRBOT_DASHBOARD_SSL_KEY") + or os.environ.get("DASHBOARD_SSL_KEY") or ssl_config.get("key_file", "") ) ca_certs = ( - os.environ.get("DASHBOARD_SSL_CA_CERTS") - or os.environ.get("ASTRBOT_DASHBOARD_SSL_CA_CERTS") + os.environ.get("ASTRBOT_DASHBOARD_SSL_CA_CERTS") + or os.environ.get("DASHBOARD_SSL_CA_CERTS") or ssl_config.get("ca_certs", "") )