mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat: improve cli admin setup and api startup logs
This commit is contained in:
@@ -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.")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user