feat: improve cli admin setup and api startup logs

This commit is contained in:
LIghtJUNction
2026-03-18 18:21:49 +08:00
parent 14ec513b0d
commit e8c234f0cf
3 changed files with 172 additions and 22 deletions

View File

@@ -11,6 +11,11 @@ from astrbot.core.utils.astrbot_path import astrbot_paths
from ..utils import check_astrbot_root
def hash_dashboard_password(value: str) -> str:
"""Hash Dashboard password for storage."""
return hashlib.md5(value.encode()).hexdigest()
def _validate_log_level(value: str) -> str:
"""Validate log level"""
value = value.upper()
@@ -43,7 +48,7 @@ def _validate_dashboard_password(value: str) -> str:
"""Validate Dashboard password"""
if not value:
raise click.ClickException("Password cannot be empty")
return hashlib.md5(value.encode()).hexdigest()
return hash_dashboard_password(value)
def _validate_timezone(value: str) -> str:
@@ -110,6 +115,11 @@ def _save_config(config: dict[str, Any]) -> None:
)
def ensure_config_file() -> dict[str, Any]:
"""Ensure config file exists and return parsed config."""
return _load_config()
def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
"""Set a value in a nested dictionary"""
parts = path.split(".")
@@ -132,6 +142,34 @@ def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
return obj
def prompt_dashboard_password(prompt: str = "Dashboard password") -> str:
"""Prompt for dashboard password with confirmation."""
password = click.prompt(
prompt,
hide_input=True,
confirmation_prompt=True,
type=str,
)
return _validate_dashboard_password(password)
def set_dashboard_credentials(
config: dict[str, Any],
*,
username: str | None = None,
password_hash: str | None = None,
) -> None:
"""Update dashboard credentials in config."""
if username is not None:
_set_nested_item(
config,
"dashboard.username",
_validate_dashboard_username(username),
)
if password_hash is not None:
_set_nested_item(config, "dashboard.password", password_hash)
@click.group(name="conf")
def conf() -> None:
"""Configuration management commands
@@ -213,3 +251,32 @@ def get_config(key: str | None = None) -> None:
click.echo(f" {key}: {value}")
except (KeyError, TypeError):
pass
@conf.command(name="password")
@click.option("-u", "--username", type=str, help="Update dashboard username as well")
@click.option(
"-p",
"--password",
type=str,
help="Set dashboard password directly without interactive prompt",
)
def set_dashboard_password(username: str | None, password: str | None) -> None:
"""Interactively manage dashboard password."""
config = _load_config()
password_hash = (
_validate_dashboard_password(password)
if password is not None
else prompt_dashboard_password()
)
set_dashboard_credentials(
config,
username=username.strip() if username is not None else None,
password_hash=password_hash,
)
_save_config(config)
if username is not None:
click.echo(f"Dashboard username updated: {username.strip()}")
click.echo("Dashboard password updated.")

View File

@@ -1,17 +1,30 @@
import asyncio
import json
import os
from pathlib import Path
import click
from filelock import FileLock, Timeout
from astrbot.core.config.default import DEFAULT_CONFIG
from astrbot.core.utils.astrbot_path import astrbot_paths
from ..utils import check_dashboard
from .cmd_conf import (
_validate_dashboard_password,
ensure_config_file,
prompt_dashboard_password,
set_dashboard_credentials,
)
async def initialize_astrbot(
astrbot_root: Path, *, yes: bool, backend_only: bool
astrbot_root: Path,
*,
yes: bool,
backend_only: bool,
admin_username: str | None,
admin_password: str | None,
) -> None:
"""Execute AstrBot initialization logic"""
dot_astrbot = astrbot_root / ".astrbot"
@@ -38,6 +51,44 @@ async def initialize_astrbot(
f"{'Created' if not path.exists() else f'{name} Directory exists'}: {path}"
)
config_path = astrbot_root / "data" / "cmd_config.json"
if not config_path.exists():
config_path.write_text(
json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
encoding="utf-8-sig",
)
click.echo(f"Created config file: {config_path}")
if admin_password and not admin_username:
raise click.ClickException(
"--admin-password requires --admin-username to be provided"
)
if admin_username:
password_hash = (
_validate_dashboard_password(admin_password)
if admin_password is not None
else None
)
if password_hash is None:
if yes or os.environ.get("ASTRBOT_SYSTEMD") == "1":
raise click.ClickException(
"Non-interactive init requires --admin-password when --admin-username is set"
)
password_hash = prompt_dashboard_password("Dashboard admin password")
config = ensure_config_file()
set_dashboard_credentials(
config,
username=admin_username.strip(),
password_hash=password_hash,
)
config_path.write_text(
json.dumps(config, ensure_ascii=False, indent=2),
encoding="utf-8-sig",
)
click.echo(f"Configured dashboard admin username: {admin_username.strip()}")
if not backend_only and (
yes
or click.confirm(
@@ -57,8 +108,26 @@ async def initialize_astrbot(
@click.command()
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
@click.option("--backend-only", "-b", is_flag=True, help="Only initialize the backend")
@click.option("--backup", help="Initialize from backup file", type=str)
def init(yes: bool, backend_only: bool, backup: str | None) -> None:
@click.option("--backup", "-f", help="Initialize from backup file", type=str)
@click.option(
"-u",
"--admin-username",
type=str,
help="Set dashboard admin username during initialization",
)
@click.option(
"-p",
"--admin-password",
type=str,
help="Set dashboard admin password during initialization without prompting",
)
def init(
yes: bool,
backend_only: bool,
backup: str | None,
admin_username: str | None,
admin_password: str | None,
) -> None:
"""Initialize AstrBot"""
click.echo("Initializing AstrBot...")
@@ -72,7 +141,13 @@ def init(yes: bool, backend_only: bool, backup: str | None) -> None:
try:
with lock.acquire():
asyncio.run(
initialize_astrbot(astrbot_root, yes=yes, backend_only=backend_only)
initialize_astrbot(
astrbot_root,
yes=yes,
backend_only=backend_only,
admin_username=admin_username,
admin_password=admin_password,
)
)
if backup:

View File

@@ -410,21 +410,19 @@ class AstrBotDashboard:
)
scheme = "https" if ssl_enable else "http"
display_host = f"[{host}]" if ":" in host else host
binds: list[str] = [self._build_bind(host, port)]
if host == "::" and platform.system() in ("Windows", "Darwin"):
binds.append(self._build_bind("0.0.0.0", port))
if self.enable_webui:
logger.info(
"正在启动 WebUI + API, 监听地址: %s://%s:%s",
scheme,
display_host,
port,
"正在启动 WebUI + API, 监听: %s",
", ".join(f"{scheme}://{bind}" for bind in binds),
)
else:
logger.info(
"正在启动 API Server (WebUI 已分离), 监听地址: %s://%s:%s",
scheme,
display_host,
port,
"正在启动 API Server (WebUI 已分离), 监听: %s",
", ".join(f"{scheme}://{bind}" for bind in binds),
)
check_hosts = {host}
@@ -435,14 +433,10 @@ class AstrBotDashboard:
info = self.get_process_using_port(port)
raise RuntimeError(f"端口 {port} 已被占用\n{info}")
if self.enable_webui:
self._print_access_urls(host, port, scheme)
self._print_access_urls(host, port, scheme, self.enable_webui)
# 配置 Hypercorn
config = HyperConfig()
binds: list[str] = [self._build_bind(host, port)]
if host == "::" and platform.system() in ("Windows", "Darwin"):
binds.append(self._build_bind("0.0.0.0", port))
config.bind = binds
if ssl_enable:
@@ -500,10 +494,17 @@ class AstrBotDashboard:
except ValueError:
return f"{host}:{port}"
def _print_access_urls(self, host: str, port: int, scheme: str = "http") -> None:
def _print_access_urls(
self,
host: str,
port: int,
scheme: str = "http",
enable_webui: bool = True,
) -> None:
local_ips: list[IPv4Address | IPv6Address] = get_local_ip_addresses()
mode_label = "WebUI + API" if enable_webui else "API Server (WebUI 已分离)"
parts = [f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动\n\n"]
parts = [f"\n ✨✨✨\n AstrBot v{VERSION} {mode_label} 已启动\n\n"]
parts.append(f" ➜ 本地: {scheme}://localhost:{port}\n")
@@ -518,8 +519,15 @@ class AstrBotDashboard:
display_url = f"{scheme}://{ip}:{port}"
parts.append(f" ➜ 网络: {display_url}\n")
else:
if ":" in host:
parts.append(f" ➜ 指定监听: {scheme}://[{host}]:{port}\n")
else:
parts.append(f" ➜ 指定监听: {scheme}://{host}:{port}\n")
parts.append(" ➜ 默认用户名和密码: astrbot\n ✨✨✨\n")
if enable_webui:
parts.append(" ➜ 默认用户名和密码: astrbot\n")
parts.append(" ✨✨✨\n")
if not local_ips:
parts.append(