mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-01 18:20:16 +08:00
Compare commits
8 Commits
codex/prep
...
feat/servi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c956894025 | ||
|
|
85e2560bf3 | ||
|
|
e2c55fa740 | ||
|
|
c2bbec7683 | ||
|
|
aa7bd5e5ad | ||
|
|
b1e1f5e6e4 | ||
|
|
250baccf5b | ||
|
|
97f0fd3de3 |
@@ -5,7 +5,7 @@ import sys
|
||||
import click
|
||||
|
||||
from . import __version__
|
||||
from .commands import conf, init, password, plug, run
|
||||
from .commands import config, init, password, plugin, run, service
|
||||
|
||||
logo_tmpl = r"""
|
||||
___ _______.___________..______ .______ ______ .___________.
|
||||
@@ -17,7 +17,23 @@ logo_tmpl = r"""
|
||||
"""
|
||||
|
||||
|
||||
@click.group()
|
||||
class AstrBotCLIGroup(click.Group):
|
||||
COMMAND_ALIASES = {
|
||||
"conf": "config",
|
||||
"plug": "plugin",
|
||||
}
|
||||
|
||||
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
||||
command = super().get_command(ctx, cmd_name)
|
||||
if command is not None:
|
||||
return command
|
||||
alias_target = self.COMMAND_ALIASES.get(cmd_name)
|
||||
if alias_target is None:
|
||||
return None
|
||||
return super().get_command(ctx, alias_target)
|
||||
|
||||
|
||||
@click.group(cls=AstrBotCLIGroup)
|
||||
@click.version_option(__version__, prog_name="AstrBot")
|
||||
def cli() -> None:
|
||||
"""The AstrBot CLI"""
|
||||
@@ -52,9 +68,10 @@ def help(command_name: str | None) -> None:
|
||||
cli.add_command(init)
|
||||
cli.add_command(run)
|
||||
cli.add_command(help)
|
||||
cli.add_command(plug)
|
||||
cli.add_command(conf)
|
||||
cli.add_command(plugin)
|
||||
cli.add_command(config)
|
||||
cli.add_command(password)
|
||||
cli.add_command(service)
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from .cmd_conf import conf
|
||||
from .cmd_conf import conf as config
|
||||
from .cmd_init import init
|
||||
from .cmd_password import password
|
||||
from .cmd_plug import plug
|
||||
from .cmd_plug import plug as plugin
|
||||
from .cmd_run import run
|
||||
from .cmd_service import service
|
||||
|
||||
__all__ = ["conf", "init", "password", "plug", "run"]
|
||||
conf = config
|
||||
plug = plugin
|
||||
|
||||
__all__ = ["config", "conf", "init", "password", "plugin", "plug", "run", "service"]
|
||||
|
||||
@@ -153,7 +153,7 @@ def _set_dashboard_password(config: dict[str, Any], raw_password: str) -> None:
|
||||
_set_nested_item(config, "dashboard.password_change_required", False)
|
||||
|
||||
|
||||
@click.group(name="conf")
|
||||
@click.group(name="config")
|
||||
def conf() -> None:
|
||||
"""Configuration management commands
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from ..utils import (
|
||||
)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.group(name="plugin")
|
||||
def plug() -> None:
|
||||
"""Plugin management"""
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ from filelock import FileLock, Timeout
|
||||
|
||||
from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root
|
||||
|
||||
DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_DASHBOARD_RESET_PASSWORD"
|
||||
|
||||
|
||||
async def run_astrbot(astrbot_root: Path) -> None:
|
||||
"""Run AstrBot"""
|
||||
@@ -28,8 +30,13 @@ async def run_astrbot(astrbot_root: Path) -> None:
|
||||
|
||||
@click.option("--reload", "-r", is_flag=True, help="Auto-reload plugins")
|
||||
@click.option("--port", "-p", help="AstrBot Dashboard port", required=False, type=str)
|
||||
@click.option(
|
||||
"--reset-password",
|
||||
is_flag=True,
|
||||
help="Force reset the dashboard initial password on startup.",
|
||||
)
|
||||
@click.command()
|
||||
def run(reload: bool, port: str) -> None:
|
||||
def run(reload: bool, port: str, reset_password: bool) -> None:
|
||||
"""Run AstrBot"""
|
||||
try:
|
||||
os.environ["ASTRBOT_CLI"] = "1"
|
||||
@@ -50,6 +57,9 @@ def run(reload: bool, port: str) -> None:
|
||||
click.echo("Plugin auto-reload enabled")
|
||||
os.environ["ASTRBOT_RELOAD"] = "1"
|
||||
|
||||
if reset_password:
|
||||
os.environ[DASHBOARD_RESET_PASSWORD_ENV] = "1"
|
||||
|
||||
lock_file = astrbot_root / "astrbot.lock"
|
||||
lock = FileLock(lock_file, timeout=5)
|
||||
with lock.acquire():
|
||||
|
||||
1175
astrbot/cli/commands/cmd_service.py
Normal file
1175
astrbot/cli/commands/cmd_service.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP
|
||||
|
||||
ASTRBOT_CONFIG_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json")
|
||||
DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD"
|
||||
DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_DASHBOARD_RESET_PASSWORD"
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
|
||||
@@ -76,21 +77,21 @@ class AstrBotConfig(dict):
|
||||
)
|
||||
# 检查配置完整性,并插入
|
||||
has_new = self.check_config_integrity(default_config, conf)
|
||||
dashboard_reset_requested = self._is_dashboard_password_reset_requested()
|
||||
if (
|
||||
"dashboard" in conf
|
||||
and isinstance(conf["dashboard"], dict)
|
||||
and not conf["dashboard"].get("pbkdf2_password")
|
||||
and not conf["dashboard"].get("password")
|
||||
):
|
||||
self._reset_generated_dashboard_password(conf)
|
||||
has_new = True
|
||||
elif (
|
||||
"dashboard" in conf
|
||||
and isinstance(conf["dashboard"], dict)
|
||||
and legacy_dashboard_password_change_required
|
||||
and conf["dashboard"].get("pbkdf2_password")
|
||||
and (
|
||||
dashboard_reset_requested
|
||||
or (
|
||||
not conf["dashboard"].get("pbkdf2_password")
|
||||
and not conf["dashboard"].get("password")
|
||||
)
|
||||
)
|
||||
):
|
||||
self._reset_generated_dashboard_password(conf)
|
||||
if dashboard_reset_requested:
|
||||
os.environ[DASHBOARD_RESET_PASSWORD_ENV] = "0"
|
||||
has_new = True
|
||||
self.update(conf)
|
||||
if has_new:
|
||||
@@ -127,6 +128,15 @@ class AstrBotConfig(dict):
|
||||
validate_dashboard_password(env_password)
|
||||
return env_password
|
||||
|
||||
@staticmethod
|
||||
def _is_dashboard_password_reset_requested() -> bool:
|
||||
return os.environ.get(DASHBOARD_RESET_PASSWORD_ENV, "").strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
|
||||
def _config_schema_to_default_config(self, schema: dict) -> dict:
|
||||
"""将 Schema 转换成 Config"""
|
||||
conf = {}
|
||||
|
||||
@@ -159,6 +159,7 @@ export default defineConfig({
|
||||
base: "/use",
|
||||
items: [
|
||||
{ text: "WebUI", link: "/webui" },
|
||||
{ text: "CLI 指令", link: "/cli" },
|
||||
{ text: "插件", link: "/plugin" },
|
||||
{ text: "内置指令", link: "/command" },
|
||||
{ text: "工具使用 Tools", link: "/function-calling" },
|
||||
@@ -403,6 +404,7 @@ export default defineConfig({
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "WebUI", link: "/webui" },
|
||||
{ text: "CLI Commands", link: "/cli" },
|
||||
{ text: "Plugins", link: "/plugin" },
|
||||
{ text: "Built-in Commands", link: "/command" },
|
||||
{ text: "Tool Use", link: "/function-calling" },
|
||||
|
||||
@@ -20,5 +20,73 @@ AstrBot requires Python 3.12 or later. Use `--python 3.12` to ensure that `uv` c
|
||||
|
||||
```bash
|
||||
uv tool install astrbot --python 3.12
|
||||
astrbot
|
||||
astrbot init # Only required for the first deployment
|
||||
astrbot run
|
||||
```
|
||||
|
||||
## Install as a System Service
|
||||
|
||||
After initialization, install AstrBot as a user-level service so it starts with the user session:
|
||||
|
||||
```bash
|
||||
astrbot service install --now
|
||||
```
|
||||
|
||||
The command uses the `astrbot` executable found on `PATH` (usually generated by `uv tool install`) and uses the current directory as the AstrBot working directory. Each platform uses its native user-level service mechanism:
|
||||
|
||||
- Linux: `systemd --user`
|
||||
- macOS: `LaunchAgent`
|
||||
|
||||
> [!NOTE]
|
||||
> `astrbot service` is not supported on Windows. Use `astrbot run` in the foreground or another process manager.
|
||||
|
||||
To specify the AstrBot working directory or executable path explicitly:
|
||||
|
||||
```bash
|
||||
astrbot service install --workdir /path/to/astrbot-root --executable /path/to/astrbot --now
|
||||
```
|
||||
|
||||
To inspect the service state and WebUI health:
|
||||
|
||||
```bash
|
||||
astrbot service status
|
||||
```
|
||||
|
||||
The status output includes the service manager state, AstrBot working directory, Dashboard port, WebUI URL, WebUI accessibility, and the overall health state.
|
||||
|
||||
You can also manage the service lifecycle with:
|
||||
|
||||
```bash
|
||||
astrbot service start
|
||||
astrbot service stop
|
||||
astrbot service restart
|
||||
astrbot service uninstall
|
||||
```
|
||||
|
||||
To view service logs:
|
||||
|
||||
```bash
|
||||
astrbot service logs
|
||||
astrbot service logs -f
|
||||
```
|
||||
|
||||
On macOS, this shows stdout logs by default. To include stderr:
|
||||
|
||||
```bash
|
||||
astrbot service logs --include-stderr
|
||||
```
|
||||
|
||||
To read the AstrBot application log file at `data/logs/astrbot.log`, enable application file logging first and restart the service:
|
||||
|
||||
```bash
|
||||
astrbot service logs enable
|
||||
astrbot service restart
|
||||
astrbot service logs --source app
|
||||
```
|
||||
|
||||
To inspect or disable application file logging:
|
||||
|
||||
```bash
|
||||
astrbot service logs status
|
||||
astrbot service logs disable
|
||||
```
|
||||
|
||||
311
docs/en/use/cli.md
Normal file
311
docs/en/use/cli.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# CLI Commands
|
||||
|
||||
The AstrBot CLI initializes instances, starts AstrBot, installs background services, reads logs, updates common config values, and manages plugins.
|
||||
|
||||
If you install AstrBot with `uv`:
|
||||
|
||||
```bash
|
||||
uv tool install astrbot --python 3.12
|
||||
```
|
||||
|
||||
`uv` creates the `astrbot` executable and puts it on `PATH`. You can inspect the path with:
|
||||
|
||||
::: code-group
|
||||
|
||||
```bash [Linux / macOS]
|
||||
which astrbot
|
||||
```
|
||||
|
||||
```powershell [Windows]
|
||||
where.exe astrbot
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
> [!TIP]
|
||||
> Run the commands below from the AstrBot working directory unless the command provides a `--workdir` option.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Initialize the directory once, then start AstrBot:
|
||||
|
||||
```bash
|
||||
astrbot init
|
||||
astrbot run
|
||||
```
|
||||
|
||||
`astrbot init` creates the data directories and configuration files required by AstrBot. After initialization, use `astrbot run` for later starts.
|
||||
|
||||
## Top-Level Commands
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `astrbot init` | Initialize the current directory as an AstrBot working directory. |
|
||||
| `astrbot run` | Start AstrBot in the foreground. |
|
||||
| `astrbot service` | Install and manage AstrBot as a background service. |
|
||||
| `astrbot config` | Read or update common config values. |
|
||||
| `astrbot password` | Change the WebUI login password interactively. |
|
||||
| `astrbot plugin` | Create, install, update, remove, or search plugins. |
|
||||
| `astrbot help` | Show CLI help. |
|
||||
| `astrbot --version` | Show the AstrBot CLI version. |
|
||||
|
||||
`conf` and `plug` are compatibility aliases and still work:
|
||||
|
||||
```bash
|
||||
astrbot conf get
|
||||
astrbot plug list
|
||||
```
|
||||
|
||||
Prefer `config` and `plugin` in new docs and scripts.
|
||||
|
||||
## Start AstrBot
|
||||
|
||||
```bash
|
||||
astrbot run
|
||||
```
|
||||
|
||||
Common options:
|
||||
|
||||
| Option | Purpose |
|
||||
| --- | --- |
|
||||
| `-p, --port <PORT>` | Set the WebUI port. |
|
||||
| `-r, --reload` | Enable plugin auto-reload for plugin development. |
|
||||
| `--reset-password` | Reset the WebUI initial password on startup and print the new initial password in startup logs. |
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
astrbot run --port 6185
|
||||
astrbot run --reload
|
||||
astrbot run --reset-password
|
||||
```
|
||||
|
||||
## Background Service
|
||||
|
||||
`astrbot service` installs AstrBot as a user-level background service for long-running deployments.
|
||||
|
||||
Each platform uses its native service manager:
|
||||
|
||||
| Platform | Service manager |
|
||||
| --- | --- |
|
||||
| Linux | `systemd --user` |
|
||||
| macOS | LaunchAgent |
|
||||
|
||||
> [!NOTE]
|
||||
> `astrbot service` is not supported on Windows. Use `astrbot run` in the foreground or another process manager.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
astrbot service install --now
|
||||
```
|
||||
|
||||
By default, this command uses the `astrbot` executable found on `PATH` and the current directory as the AstrBot working directory. `--now` starts or restarts the service after installation.
|
||||
|
||||
Common options:
|
||||
|
||||
| Option | Purpose |
|
||||
| --- | --- |
|
||||
| `--name <NAME>` | Service name. Default: `astrbot`. |
|
||||
| `--workdir <DIR>` | AstrBot working directory. |
|
||||
| `--executable <PATH>` | Path to the `astrbot` executable. |
|
||||
| `--force` | Overwrite an existing service definition. |
|
||||
| `--now` | Start or restart the service after installation. |
|
||||
|
||||
If `astrbot` is not on `PATH`, pass the executable explicitly:
|
||||
|
||||
```bash
|
||||
astrbot service install --workdir /path/to/astrbot-root --executable /path/to/astrbot --now
|
||||
```
|
||||
|
||||
### Manage
|
||||
|
||||
```bash
|
||||
astrbot service start
|
||||
astrbot service stop
|
||||
astrbot service restart
|
||||
astrbot service uninstall
|
||||
```
|
||||
|
||||
These commands support `--name <NAME>` for non-default service names:
|
||||
|
||||
```bash
|
||||
astrbot service restart --name astrbot-test
|
||||
```
|
||||
|
||||
To remove a service without an interactive confirmation:
|
||||
|
||||
```bash
|
||||
astrbot service uninstall --force
|
||||
```
|
||||
|
||||
### Status
|
||||
|
||||
```bash
|
||||
astrbot service status
|
||||
```
|
||||
|
||||
The status output includes:
|
||||
|
||||
- Overall health.
|
||||
- Current platform and service manager.
|
||||
- Whether the service is installed, enabled, and running.
|
||||
- AstrBot working directory.
|
||||
- Dashboard port.
|
||||
- WebUI URL and accessibility.
|
||||
|
||||
Common options:
|
||||
|
||||
| Option | Purpose |
|
||||
| --- | --- |
|
||||
| `--name <NAME>` | Service name. Default: `astrbot`. |
|
||||
| `--workdir <DIR>` | AstrBot working directory used to read the port config. |
|
||||
| `--timeout <SECONDS>` | WebUI health probe timeout. Default: 2 seconds. |
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
astrbot service status --timeout 5
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
The CLI exposes two kinds of logs:
|
||||
|
||||
| Type | Command | Notes |
|
||||
| --- | --- | --- |
|
||||
| Service logs | `astrbot service logs` | Reads console output captured by the service manager. |
|
||||
| Application log file | `astrbot service logs --source app` | Reads `data/logs/astrbot.log`; file logging must be enabled first. |
|
||||
|
||||
### Service Logs
|
||||
|
||||
```bash
|
||||
astrbot service logs
|
||||
astrbot service logs -n 100
|
||||
astrbot service logs -f
|
||||
```
|
||||
|
||||
Common options:
|
||||
|
||||
| Option | Purpose |
|
||||
| --- | --- |
|
||||
| `--name <NAME>` | Service name. |
|
||||
| `-n, --lines <N>` | Show the latest N lines. Default: 200. |
|
||||
| `-f, --follow` | Follow log output. |
|
||||
| `--include-stderr` | Also show stderr logs on macOS. |
|
||||
|
||||
On macOS, `astrbot service logs` shows stdout logs by default, which are the `.out.log` files. Add `--include-stderr` when you also need error output.
|
||||
|
||||
### Application Log File
|
||||
|
||||
`data/logs/astrbot.log` is not written by default. Enable application file logging first, then restart AstrBot:
|
||||
|
||||
```bash
|
||||
astrbot service logs enable
|
||||
astrbot service restart
|
||||
astrbot service logs --source app
|
||||
```
|
||||
|
||||
Inspect the application log file configuration:
|
||||
|
||||
```bash
|
||||
astrbot service logs status
|
||||
```
|
||||
|
||||
Disable the application log file:
|
||||
|
||||
```bash
|
||||
astrbot service logs disable
|
||||
astrbot service restart
|
||||
```
|
||||
|
||||
Use a custom application log path:
|
||||
|
||||
```bash
|
||||
astrbot service logs enable --path logs/astrbot.log
|
||||
```
|
||||
|
||||
Relative paths are resolved from the AstrBot data directory.
|
||||
|
||||
## Config
|
||||
|
||||
`astrbot config` reads and updates common config values.
|
||||
|
||||
```bash
|
||||
astrbot config get
|
||||
astrbot config get dashboard.port
|
||||
astrbot config set dashboard.port 6185
|
||||
```
|
||||
|
||||
Supported keys:
|
||||
|
||||
| Key | Description |
|
||||
| --- | --- |
|
||||
| `timezone` | Time zone, for example `Asia/Shanghai`. |
|
||||
| `log_level` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL`. |
|
||||
| `dashboard.port` | WebUI port. |
|
||||
| `dashboard.username` | WebUI username. |
|
||||
| `dashboard.password` | WebUI password. |
|
||||
| `callback_api_base` | Callback API base URL. Must start with `http://` or `https://`. |
|
||||
|
||||
Changing the dashboard password writes the current password hashes automatically:
|
||||
|
||||
```bash
|
||||
astrbot config set dashboard.password "new-password"
|
||||
```
|
||||
|
||||
You can also use the dedicated interactive password command:
|
||||
|
||||
```bash
|
||||
astrbot password
|
||||
astrbot password --username admin
|
||||
```
|
||||
|
||||
## Plugins
|
||||
|
||||
`astrbot plugin` manages plugins under `data/plugins`.
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `astrbot plugin list` | List installed plugins. |
|
||||
| `astrbot plugin list --all` | Also show uninstalled plugins. |
|
||||
| `astrbot plugin search <QUERY>` | Search plugins. |
|
||||
| `astrbot plugin install <NAME>` | Install a plugin. |
|
||||
| `astrbot plugin update [NAME]` | Update one plugin, or all updatable plugins if no name is given. |
|
||||
| `astrbot plugin remove <NAME>` | Remove an installed plugin. |
|
||||
| `astrbot plugin new <NAME>` | Create a new plugin from the template. |
|
||||
|
||||
Use a GitHub proxy when installing or updating plugins:
|
||||
|
||||
```bash
|
||||
astrbot plugin install example-plugin --proxy https://gh-proxy.example.com/
|
||||
astrbot plugin update --proxy https://gh-proxy.example.com/
|
||||
```
|
||||
|
||||
Creating a new plugin asks for the author, description, version, and repository URL:
|
||||
|
||||
```bash
|
||||
astrbot plugin new my-plugin
|
||||
```
|
||||
|
||||
## Help
|
||||
|
||||
Show general CLI help:
|
||||
|
||||
```bash
|
||||
astrbot help
|
||||
```
|
||||
|
||||
Show help for a specific command:
|
||||
|
||||
```bash
|
||||
astrbot help service
|
||||
astrbot service --help
|
||||
astrbot service logs --help
|
||||
```
|
||||
|
||||
Show the version:
|
||||
|
||||
```bash
|
||||
astrbot --version
|
||||
```
|
||||
@@ -22,3 +22,70 @@ uv tool install astrbot --python 3.12
|
||||
astrbot init # 只需要在第一次部署时执行,后续启动不需要执行
|
||||
astrbot run
|
||||
```
|
||||
|
||||
## 安装为系统服务
|
||||
|
||||
初始化完成后,可以安装用户级服务,让 AstrBot 随用户会话自动启动:
|
||||
|
||||
```bash
|
||||
astrbot service install --now
|
||||
```
|
||||
|
||||
该命令会自动使用当前 `PATH` 中的 `astrbot` 可执行文件(通常由 `uv tool install` 生成),并将当前目录作为 AstrBot 工作目录。不同系统会使用对应的用户级服务机制:
|
||||
|
||||
- Linux:`systemd --user`
|
||||
- macOS:`LaunchAgent`
|
||||
|
||||
> [!NOTE]
|
||||
> Windows 暂不支持 `astrbot service`。请使用 `astrbot run` 前台启动,或使用其他进程管理工具。
|
||||
|
||||
如果需要指定 AstrBot 工作目录或可执行文件路径,可以使用:
|
||||
|
||||
```bash
|
||||
astrbot service install --workdir /path/to/astrbot-root --executable /path/to/astrbot --now
|
||||
```
|
||||
|
||||
查看服务状态和 WebUI 健康状态:
|
||||
|
||||
```bash
|
||||
astrbot service status
|
||||
```
|
||||
|
||||
状态输出会包含服务管理器状态、AstrBot 工作目录、Dashboard 端口、WebUI URL、WebUI 是否可访问,以及整体健康状态。
|
||||
|
||||
也可以使用以下命令管理服务生命周期:
|
||||
|
||||
```bash
|
||||
astrbot service start
|
||||
astrbot service stop
|
||||
astrbot service restart
|
||||
astrbot service uninstall
|
||||
```
|
||||
|
||||
查看服务日志:
|
||||
|
||||
```bash
|
||||
astrbot service logs
|
||||
astrbot service logs -f
|
||||
```
|
||||
|
||||
macOS 下默认只显示标准输出日志;如需同时查看 stderr:
|
||||
|
||||
```bash
|
||||
astrbot service logs --include-stderr
|
||||
```
|
||||
|
||||
如果需要查看 AstrBot 应用日志文件 `data/logs/astrbot.log`,先启用应用日志文件并重启服务:
|
||||
|
||||
```bash
|
||||
astrbot service logs enable
|
||||
astrbot service restart
|
||||
astrbot service logs --source app
|
||||
```
|
||||
|
||||
查看或关闭应用日志文件:
|
||||
|
||||
```bash
|
||||
astrbot service logs status
|
||||
astrbot service logs disable
|
||||
```
|
||||
|
||||
311
docs/zh/use/cli.md
Normal file
311
docs/zh/use/cli.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# CLI 指令
|
||||
|
||||
AstrBot CLI 用于初始化实例、启动 AstrBot、安装后台服务、查看日志、修改常用配置和管理插件。
|
||||
|
||||
如果你使用 `uv` 安装:
|
||||
|
||||
```bash
|
||||
uv tool install astrbot --python 3.12
|
||||
```
|
||||
|
||||
`uv` 会生成 `astrbot` 可执行文件,并把它放到 `PATH` 中。可以用下面的命令确认路径:
|
||||
|
||||
::: code-group
|
||||
|
||||
```bash [Linux / macOS]
|
||||
which astrbot
|
||||
```
|
||||
|
||||
```powershell [Windows]
|
||||
where.exe astrbot
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
> [!TIP]
|
||||
> 下面的命令都需要在 AstrBot 工作目录中执行,除非命令提供了 `--workdir` 选项。
|
||||
|
||||
## 快速开始
|
||||
|
||||
第一次部署时先初始化目录,再启动 AstrBot:
|
||||
|
||||
```bash
|
||||
astrbot init
|
||||
astrbot run
|
||||
```
|
||||
|
||||
`astrbot init` 会在当前目录创建 AstrBot 所需的数据目录和配置文件。初始化完成后,后续启动只需要执行 `astrbot run`。
|
||||
|
||||
## 顶层指令
|
||||
|
||||
| 指令 | 用途 |
|
||||
| --- | --- |
|
||||
| `astrbot init` | 初始化当前目录为 AstrBot 工作目录。 |
|
||||
| `astrbot run` | 在前台启动 AstrBot。 |
|
||||
| `astrbot service` | 安装和管理 AstrBot 后台服务。 |
|
||||
| `astrbot config` | 查看或修改常用配置项。 |
|
||||
| `astrbot password` | 交互式修改 WebUI 登录密码。 |
|
||||
| `astrbot plugin` | 创建、安装、更新、删除或搜索插件。 |
|
||||
| `astrbot help` | 查看 CLI 帮助。 |
|
||||
| `astrbot --version` | 查看 AstrBot CLI 版本。 |
|
||||
|
||||
`conf` 和 `plug` 是兼容别名,仍然可用:
|
||||
|
||||
```bash
|
||||
astrbot conf get
|
||||
astrbot plug list
|
||||
```
|
||||
|
||||
推荐在新文档和脚本中使用 `config` 和 `plugin`。
|
||||
|
||||
## 启动 AstrBot
|
||||
|
||||
```bash
|
||||
astrbot run
|
||||
```
|
||||
|
||||
常用选项:
|
||||
|
||||
| 选项 | 用途 |
|
||||
| --- | --- |
|
||||
| `-p, --port <PORT>` | 指定 WebUI 端口。 |
|
||||
| `-r, --reload` | 启用插件自动重载,适合插件开发调试。 |
|
||||
| `--reset-password` | 启动时重置 WebUI 初始密码,并在启动日志中打印新的初始密码。 |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
astrbot run --port 6185
|
||||
astrbot run --reload
|
||||
astrbot run --reset-password
|
||||
```
|
||||
|
||||
## 后台服务
|
||||
|
||||
`astrbot service` 可以把 AstrBot 安装为用户级后台服务,适合长期运行。
|
||||
|
||||
不同系统会使用对应的服务管理机制:
|
||||
|
||||
| 系统 | 服务管理器 |
|
||||
| --- | --- |
|
||||
| Linux | `systemd --user` |
|
||||
| macOS | LaunchAgent |
|
||||
|
||||
> [!NOTE]
|
||||
> Windows 暂不支持 `astrbot service`。请使用 `astrbot run` 前台启动,或使用其他进程管理工具。
|
||||
|
||||
### 安装服务
|
||||
|
||||
```bash
|
||||
astrbot service install --now
|
||||
```
|
||||
|
||||
该命令默认使用当前 `PATH` 中的 `astrbot` 可执行文件,并把当前目录作为 AstrBot 工作目录。`--now` 表示安装后立即启动或重启服务。
|
||||
|
||||
常用选项:
|
||||
|
||||
| 选项 | 用途 |
|
||||
| --- | --- |
|
||||
| `--name <NAME>` | 指定服务名,默认 `astrbot`。 |
|
||||
| `--workdir <DIR>` | 指定 AstrBot 工作目录。 |
|
||||
| `--executable <PATH>` | 指定 `astrbot` 可执行文件路径。 |
|
||||
| `--force` | 覆盖已有服务定义。 |
|
||||
| `--now` | 安装后立即启动或重启服务。 |
|
||||
|
||||
如果 `astrbot` 不在 `PATH` 中,可以显式指定可执行文件:
|
||||
|
||||
```bash
|
||||
astrbot service install --workdir /path/to/astrbot-root --executable /path/to/astrbot --now
|
||||
```
|
||||
|
||||
### 管理服务
|
||||
|
||||
```bash
|
||||
astrbot service start
|
||||
astrbot service stop
|
||||
astrbot service restart
|
||||
astrbot service uninstall
|
||||
```
|
||||
|
||||
这些命令都支持 `--name <NAME>`,用于管理非默认服务名:
|
||||
|
||||
```bash
|
||||
astrbot service restart --name astrbot-test
|
||||
```
|
||||
|
||||
卸载服务时,如果不希望交互确认,可以使用:
|
||||
|
||||
```bash
|
||||
astrbot service uninstall --force
|
||||
```
|
||||
|
||||
### 查看服务状态
|
||||
|
||||
```bash
|
||||
astrbot service status
|
||||
```
|
||||
|
||||
状态输出会包含:
|
||||
|
||||
- 整体健康状态。
|
||||
- 当前系统和服务管理器。
|
||||
- 服务是否已安装、是否启用、当前运行状态。
|
||||
- AstrBot 工作目录。
|
||||
- Dashboard 端口。
|
||||
- WebUI URL 和是否可访问。
|
||||
|
||||
常用选项:
|
||||
|
||||
| 选项 | 用途 |
|
||||
| --- | --- |
|
||||
| `--name <NAME>` | 指定服务名,默认 `astrbot`。 |
|
||||
| `--workdir <DIR>` | 指定 AstrBot 工作目录,用于读取端口配置。 |
|
||||
| `--timeout <SECONDS>` | 指定 WebUI 健康检查超时时间,默认 2 秒。 |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
astrbot service status --timeout 5
|
||||
```
|
||||
|
||||
## 日志
|
||||
|
||||
AstrBot CLI 中有两类日志:
|
||||
|
||||
| 类型 | 命令 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 服务日志 | `astrbot service logs` | 查看服务管理器捕获的控制台输出。 |
|
||||
| 应用日志文件 | `astrbot service logs --source app` | 查看 `data/logs/astrbot.log`,需要先启用文件日志。 |
|
||||
|
||||
### 查看服务日志
|
||||
|
||||
```bash
|
||||
astrbot service logs
|
||||
astrbot service logs -n 100
|
||||
astrbot service logs -f
|
||||
```
|
||||
|
||||
常用选项:
|
||||
|
||||
| 选项 | 用途 |
|
||||
| --- | --- |
|
||||
| `--name <NAME>` | 指定服务名。 |
|
||||
| `-n, --lines <N>` | 显示最近 N 行,默认 200。 |
|
||||
| `-f, --follow` | 持续跟随日志输出。 |
|
||||
| `--include-stderr` | 在 macOS 上同时显示 stderr 日志。 |
|
||||
|
||||
macOS 下,`astrbot service logs` 默认只显示标准输出日志,也就是 `.out.log`。如果需要同时查看错误输出,再加 `--include-stderr`。
|
||||
|
||||
### 启用应用日志文件
|
||||
|
||||
`data/logs/astrbot.log` 默认不会写入。需要先启用应用日志文件,然后重启 AstrBot:
|
||||
|
||||
```bash
|
||||
astrbot service logs enable
|
||||
astrbot service restart
|
||||
astrbot service logs --source app
|
||||
```
|
||||
|
||||
查看应用日志文件配置:
|
||||
|
||||
```bash
|
||||
astrbot service logs status
|
||||
```
|
||||
|
||||
关闭应用日志文件:
|
||||
|
||||
```bash
|
||||
astrbot service logs disable
|
||||
astrbot service restart
|
||||
```
|
||||
|
||||
自定义应用日志文件路径:
|
||||
|
||||
```bash
|
||||
astrbot service logs enable --path logs/astrbot.log
|
||||
```
|
||||
|
||||
相对路径会以 AstrBot 数据目录为基准解析。
|
||||
|
||||
## 配置
|
||||
|
||||
`astrbot config` 用于查看和修改常用配置项。
|
||||
|
||||
```bash
|
||||
astrbot config get
|
||||
astrbot config get dashboard.port
|
||||
astrbot config set dashboard.port 6185
|
||||
```
|
||||
|
||||
支持的配置项:
|
||||
|
||||
| 配置项 | 说明 |
|
||||
| --- | --- |
|
||||
| `timezone` | 时区,例如 `Asia/Shanghai`。 |
|
||||
| `log_level` | 日志等级:`DEBUG`、`INFO`、`WARNING`、`ERROR`、`CRITICAL`。 |
|
||||
| `dashboard.port` | WebUI 端口。 |
|
||||
| `dashboard.username` | WebUI 用户名。 |
|
||||
| `dashboard.password` | WebUI 密码。 |
|
||||
| `callback_api_base` | 回调 API 基础地址,需要以 `http://` 或 `https://` 开头。 |
|
||||
|
||||
修改密码时会自动写入新版密码哈希:
|
||||
|
||||
```bash
|
||||
astrbot config set dashboard.password "new-password"
|
||||
```
|
||||
|
||||
也可以使用专门的交互式密码指令:
|
||||
|
||||
```bash
|
||||
astrbot password
|
||||
astrbot password --username admin
|
||||
```
|
||||
|
||||
## 插件
|
||||
|
||||
`astrbot plugin` 用于管理 `data/plugins` 下的插件。
|
||||
|
||||
| 指令 | 用途 |
|
||||
| --- | --- |
|
||||
| `astrbot plugin list` | 查看已安装插件。 |
|
||||
| `astrbot plugin list --all` | 同时显示未安装插件。 |
|
||||
| `astrbot plugin search <QUERY>` | 搜索插件。 |
|
||||
| `astrbot plugin install <NAME>` | 安装插件。 |
|
||||
| `astrbot plugin update [NAME]` | 更新指定插件;不传名称时更新所有可更新插件。 |
|
||||
| `astrbot plugin remove <NAME>` | 删除已安装插件。 |
|
||||
| `astrbot plugin new <NAME>` | 基于模板创建新插件。 |
|
||||
|
||||
安装或更新插件时可以使用 GitHub 代理:
|
||||
|
||||
```bash
|
||||
astrbot plugin install example-plugin --proxy https://gh-proxy.example.com/
|
||||
astrbot plugin update --proxy https://gh-proxy.example.com/
|
||||
```
|
||||
|
||||
创建新插件会交互式询问作者、描述、版本和仓库地址:
|
||||
|
||||
```bash
|
||||
astrbot plugin new my-plugin
|
||||
```
|
||||
|
||||
## 帮助
|
||||
|
||||
查看全部 CLI 帮助:
|
||||
|
||||
```bash
|
||||
astrbot help
|
||||
```
|
||||
|
||||
查看指定指令帮助:
|
||||
|
||||
```bash
|
||||
astrbot help service
|
||||
astrbot service --help
|
||||
astrbot service logs --help
|
||||
```
|
||||
|
||||
查看版本:
|
||||
|
||||
```bash
|
||||
astrbot --version
|
||||
```
|
||||
15
main.py
15
main.py
@@ -9,6 +9,16 @@ import runtime_bootstrap
|
||||
|
||||
runtime_bootstrap.initialize_runtime_bootstrap()
|
||||
|
||||
DASHBOARD_RESET_PASSWORD_ENV = "ASTRBOT_DASHBOARD_RESET_PASSWORD"
|
||||
|
||||
|
||||
def _prime_startup_flags(argv: list[str]) -> None:
|
||||
if "--reset-password" in argv:
|
||||
os.environ[DASHBOARD_RESET_PASSWORD_ENV] = "1"
|
||||
|
||||
|
||||
_prime_startup_flags(sys.argv[1:])
|
||||
|
||||
from astrbot.core import LogBroker, LogManager, db_helper, logger # noqa: E402
|
||||
from astrbot.core.config.default import VERSION # noqa: E402
|
||||
from astrbot.core.initial_loader import InitialLoader # noqa: E402
|
||||
@@ -140,6 +150,11 @@ if __name__ == "__main__":
|
||||
help="Specify the directory path for WebUI static files",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reset-password",
|
||||
action="store_true",
|
||||
help="Force reset the dashboard initial password on startup.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
check_env()
|
||||
|
||||
25
tests/test_cli_command_aliases.py
Normal file
25
tests/test_cli_command_aliases.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from click.testing import CliRunner
|
||||
|
||||
from astrbot.cli.__main__ import cli
|
||||
|
||||
|
||||
def test_top_level_help_uses_product_command_names():
|
||||
result = CliRunner().invoke(cli, ["help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "config" in result.output
|
||||
assert "plugin" in result.output
|
||||
assert " conf " not in result.output
|
||||
assert " plug " not in result.output
|
||||
|
||||
|
||||
def test_legacy_config_and_plugin_aliases_still_work():
|
||||
runner = CliRunner()
|
||||
|
||||
config_result = runner.invoke(cli, ["help", "conf"])
|
||||
plugin_result = runner.invoke(cli, ["help", "plug"])
|
||||
|
||||
assert config_result.exit_code == 0
|
||||
assert "Configuration management commands" in config_result.output
|
||||
assert plugin_result.exit_code == 0
|
||||
assert "Plugin management" in plugin_result.output
|
||||
22
tests/test_cli_run.py
Normal file
22
tests/test_cli_run.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from astrbot.cli.commands import cmd_run
|
||||
|
||||
|
||||
def test_run_reset_password_sets_startup_env(monkeypatch, tmp_path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv(cmd_run.DASHBOARD_RESET_PASSWORD_ENV, raising=False)
|
||||
(tmp_path / ".astrbot").touch()
|
||||
observed_reset_flags = []
|
||||
|
||||
async def fake_run_astrbot(_astrbot_root):
|
||||
observed_reset_flags.append(os.environ.get(cmd_run.DASHBOARD_RESET_PASSWORD_ENV))
|
||||
|
||||
monkeypatch.setattr(cmd_run, "run_astrbot", fake_run_astrbot)
|
||||
|
||||
result = CliRunner().invoke(cmd_run.run, ["--reset-password"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert observed_reset_flags == ["1"]
|
||||
313
tests/test_cli_service.py
Normal file
313
tests/test_cli_service.py
Normal file
@@ -0,0 +1,313 @@
|
||||
import json
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from astrbot.cli.__main__ import cli
|
||||
from astrbot.cli.commands import cmd_service
|
||||
from astrbot.cli.commands.cmd_service import (
|
||||
ServiceState,
|
||||
WebUIStatus,
|
||||
_build_launchd_plist,
|
||||
_build_systemd_unit,
|
||||
_check_webui,
|
||||
_get_app_log_config,
|
||||
_health_label,
|
||||
_load_dashboard_port,
|
||||
_load_or_init_config,
|
||||
service,
|
||||
)
|
||||
|
||||
|
||||
class _HealthyHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"ok")
|
||||
|
||||
def log_message(self, *_args):
|
||||
return
|
||||
|
||||
|
||||
def test_service_command_is_registered():
|
||||
result = CliRunner().invoke(cli, ["help", "service"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "install" in result.output
|
||||
assert "logs" in result.output
|
||||
assert "restart" in result.output
|
||||
assert "status" in result.output
|
||||
assert "start" in result.output
|
||||
assert "stop" in result.output
|
||||
assert "uninstall" in result.output
|
||||
|
||||
|
||||
def test_service_logs_group_exposes_log_file_controls():
|
||||
result = CliRunner().invoke(service, ["logs", "--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "enable" in result.output
|
||||
assert "disable" in result.output
|
||||
assert "status" in result.output
|
||||
|
||||
|
||||
def test_service_install_requires_initialized_root(monkeypatch, tmp_path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
result = CliRunner().invoke(service, ["install", "--executable", "astrbot"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "Use 'astrbot init' before installing the service" in result.output
|
||||
|
||||
|
||||
def test_service_install_rejects_windows(monkeypatch, tmp_path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setattr(cmd_service.platform, "system", lambda: "Windows")
|
||||
|
||||
result = CliRunner().invoke(service, ["install", "--executable", "astrbot"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "Unsupported platform: Windows" in result.output
|
||||
|
||||
|
||||
def test_systemd_unit_uses_astrbot_executable_and_working_directory():
|
||||
unit = _build_systemd_unit(
|
||||
"astrbot",
|
||||
Path("/home/astrbot/.local/bin/astrbot"),
|
||||
Path("/home/astrbot/AstrBot Root"),
|
||||
)
|
||||
|
||||
assert 'WorkingDirectory="/home/astrbot/AstrBot Root"' in unit
|
||||
assert "ExecStart=/home/astrbot/.local/bin/astrbot run" in unit
|
||||
assert "Environment=PYTHONUNBUFFERED=1" in unit
|
||||
|
||||
|
||||
def test_launchd_plist_uses_astrbot_executable_and_working_directory():
|
||||
plist = _build_launchd_plist(
|
||||
"astrbot",
|
||||
Path("/Users/astrbot/.local/bin/astrbot"),
|
||||
Path("/Users/astrbot/AstrBot"),
|
||||
Path("/Users/astrbot/Library/Logs/AstrBot"),
|
||||
)
|
||||
|
||||
assert plist["Label"] == "app.astrbot.astrbot"
|
||||
assert plist["ProgramArguments"] == ["/Users/astrbot/.local/bin/astrbot", "run"]
|
||||
assert plist["WorkingDirectory"] == "/Users/astrbot/AstrBot"
|
||||
assert plist["EnvironmentVariables"] == {"PYTHONUNBUFFERED": "1"}
|
||||
|
||||
|
||||
def test_launch_agent_start_waits_until_loaded_before_kickstart(monkeypatch, tmp_path):
|
||||
plist_path = tmp_path / "app.astrbot.astrbot.plist"
|
||||
plist_path.touch()
|
||||
events = []
|
||||
loaded_states = [False, False, True]
|
||||
|
||||
monkeypatch.setattr(cmd_service.shutil, "which", lambda name: "/bin/launchctl")
|
||||
monkeypatch.setattr(cmd_service, "_launch_agent_path", lambda _name: plist_path)
|
||||
monkeypatch.setattr(cmd_service.time, "sleep", lambda _seconds: None)
|
||||
|
||||
def fake_run_capture(command):
|
||||
if command[1] == "print":
|
||||
events.append("print")
|
||||
loaded = loaded_states.pop(0) if loaded_states else True
|
||||
return cmd_service.subprocess.CompletedProcess(
|
||||
command,
|
||||
0 if loaded else 113,
|
||||
stdout="",
|
||||
stderr="not loaded",
|
||||
)
|
||||
if command[1] == "kickstart":
|
||||
events.append("kickstart")
|
||||
return cmd_service.subprocess.CompletedProcess(command, 0)
|
||||
raise AssertionError(f"Unexpected capture command: {command}")
|
||||
|
||||
def fake_run_checked(command, _failure_message):
|
||||
events.append(command[1])
|
||||
|
||||
monkeypatch.setattr(cmd_service, "_run_capture", fake_run_capture)
|
||||
monkeypatch.setattr(cmd_service, "_run_checked", fake_run_checked)
|
||||
|
||||
cmd_service._start_launch_agent("astrbot")
|
||||
|
||||
assert "bootstrap" in events
|
||||
assert "enable" in events
|
||||
assert "kickstart" in events
|
||||
assert events.index("bootstrap") < events.index("kickstart")
|
||||
|
||||
|
||||
def test_load_dashboard_port_reads_cmd_config(tmp_path):
|
||||
config_path = tmp_path / "data" / "cmd_config.json"
|
||||
config_path.parent.mkdir()
|
||||
config_path.write_text(
|
||||
json.dumps({"dashboard": {"port": 7788}}),
|
||||
encoding="utf-8-sig",
|
||||
)
|
||||
|
||||
dashboard_port = _load_dashboard_port(tmp_path)
|
||||
|
||||
assert dashboard_port.port == 7788
|
||||
assert dashboard_port.detail is None
|
||||
|
||||
|
||||
def test_check_webui_reports_accessible_http_response():
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), _HealthyHandler)
|
||||
thread = Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
|
||||
try:
|
||||
webui_status = _check_webui(server.server_port, timeout=1.0)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=1)
|
||||
|
||||
assert webui_status.accessible is True
|
||||
assert webui_status.status_code == 200
|
||||
|
||||
|
||||
def test_health_label_requires_service_and_webui():
|
||||
active = ServiceState(manager="systemd --user", installed=True, state="active")
|
||||
inactive = ServiceState(manager="systemd --user", installed=True, state="inactive")
|
||||
reachable = WebUIStatus(url="http://127.0.0.1:6185/", accessible=True)
|
||||
unreachable = WebUIStatus(url="http://127.0.0.1:6185/", accessible=False)
|
||||
|
||||
assert _health_label(active, reachable) == "healthy"
|
||||
assert _health_label(active, unreachable) == "degraded"
|
||||
assert _health_label(inactive, reachable) == "degraded"
|
||||
assert _health_label(inactive, unreachable) == "unhealthy"
|
||||
|
||||
|
||||
def test_service_status_reports_port_and_webui_health(monkeypatch, tmp_path):
|
||||
(tmp_path / ".astrbot").touch()
|
||||
config_path = tmp_path / "data" / "cmd_config.json"
|
||||
config_path.parent.mkdir()
|
||||
config_path.write_text(
|
||||
json.dumps({"dashboard": {"port": 7788}}),
|
||||
encoding="utf-8-sig",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
cmd_service,
|
||||
"_get_service_state",
|
||||
lambda _name: ServiceState(
|
||||
manager="systemd --user",
|
||||
installed=True,
|
||||
state="active",
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cmd_service,
|
||||
"_check_webui",
|
||||
lambda port, _timeout: WebUIStatus(
|
||||
url=f"http://127.0.0.1:{port}/",
|
||||
accessible=True,
|
||||
status_code=200,
|
||||
detail="HTTP 200",
|
||||
),
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(service, ["status", "--workdir", str(tmp_path)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Health: healthy" in result.output
|
||||
assert "Dashboard port: 7788" in result.output
|
||||
assert "WebUI accessible: yes" in result.output
|
||||
assert "WebUI HTTP status: 200" in result.output
|
||||
|
||||
|
||||
def test_service_start_dispatches_to_platform_control(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
cmd_service,
|
||||
"_control_service",
|
||||
lambda name, action: calls.append((name, action)),
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(service, ["start", "--name", "astrbot-test"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert calls == [("astrbot-test", "start")]
|
||||
assert "Started service: astrbot-test" in result.output
|
||||
|
||||
|
||||
def test_service_uninstall_requires_confirmation(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
cmd_service,
|
||||
"_uninstall_service",
|
||||
lambda name: calls.append(name) or name,
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(service, ["uninstall"], input="n\n")
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_service_logs_source_app_reads_application_log(monkeypatch, tmp_path):
|
||||
(tmp_path / ".astrbot").touch()
|
||||
log_path = tmp_path / "data" / "logs" / "astrbot.log"
|
||||
log_path.parent.mkdir(parents=True)
|
||||
log_path.write_text("first\nsecond\nthird\n", encoding="utf-8")
|
||||
|
||||
result = CliRunner().invoke(
|
||||
service,
|
||||
["logs", "--source", "app", "--workdir", str(tmp_path), "--lines", "2"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "first" not in result.output
|
||||
assert "second" in result.output
|
||||
assert "third" in result.output
|
||||
|
||||
|
||||
def test_service_logs_hides_stderr_by_default(monkeypatch, tmp_path):
|
||||
out_log = tmp_path / "astrbot.out.log"
|
||||
err_log = tmp_path / "astrbot.err.log"
|
||||
out_log.write_text("normal output\n", encoding="utf-8")
|
||||
err_log.write_text("stderr output\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(cmd_service.platform, "system", lambda: "Darwin")
|
||||
monkeypatch.setattr(
|
||||
cmd_service,
|
||||
"_service_log_paths",
|
||||
lambda _name: (out_log, err_log),
|
||||
)
|
||||
|
||||
default_result = CliRunner().invoke(service, ["logs", "--lines", "10"])
|
||||
stderr_result = CliRunner().invoke(
|
||||
service,
|
||||
["logs", "--lines", "10", "--include-stderr"],
|
||||
)
|
||||
|
||||
assert default_result.exit_code == 0
|
||||
assert "normal output" in default_result.output
|
||||
assert "stderr output" not in default_result.output
|
||||
assert stderr_result.exit_code == 0
|
||||
assert "normal output" in stderr_result.output
|
||||
assert "stderr output" in stderr_result.output
|
||||
|
||||
|
||||
def test_service_app_log_enable_updates_config(tmp_path):
|
||||
(tmp_path / ".astrbot").touch()
|
||||
|
||||
result = CliRunner().invoke(
|
||||
service,
|
||||
[
|
||||
"logs",
|
||||
"enable",
|
||||
"--workdir",
|
||||
str(tmp_path),
|
||||
"--path",
|
||||
"logs/custom.log",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
config = _load_or_init_config(tmp_path)
|
||||
log_config = _get_app_log_config(tmp_path, config)
|
||||
assert log_config.enabled is True
|
||||
assert log_config.configured_path == "logs/custom.log"
|
||||
assert log_config.path == tmp_path / "data" / "logs" / "custom.log"
|
||||
@@ -10,6 +10,8 @@ from astrbot.core.config.default import DEFAULT_VALUE_MAP
|
||||
from astrbot.core.config.i18n_utils import ConfigMetadataI18n
|
||||
from astrbot.core.utils.auth_password import (
|
||||
DEFAULT_DASHBOARD_PASSWORD,
|
||||
hash_dashboard_password,
|
||||
hash_legacy_dashboard_password,
|
||||
validate_dashboard_password,
|
||||
verify_dashboard_password,
|
||||
)
|
||||
@@ -276,15 +278,20 @@ class TestAstrBotConfigLoad:
|
||||
default_config=default_config,
|
||||
)
|
||||
|
||||
def test_legacy_password_change_required_rotates_and_keeps_config_flag(
|
||||
def test_password_change_required_keeps_existing_password(
|
||||
self, temp_config_path
|
||||
):
|
||||
"""Test that the setup flag stays in dashboard config."""
|
||||
"""Test that the setup flag no longer rotates the initial password."""
|
||||
existing_password = "ExistingPass123"
|
||||
existing_pbkdf2_password = hash_dashboard_password(existing_password)
|
||||
existing_legacy_password = hash_legacy_dashboard_password(existing_password)
|
||||
default_config = {
|
||||
"dashboard": {
|
||||
"username": "astrbot",
|
||||
"password": "",
|
||||
"pbkdf2_password": "",
|
||||
"password_storage_upgraded": False,
|
||||
"password_change_required": False,
|
||||
},
|
||||
}
|
||||
with open(temp_config_path, "w", encoding="utf-8") as f:
|
||||
@@ -292,8 +299,9 @@ class TestAstrBotConfigLoad:
|
||||
{
|
||||
"dashboard": {
|
||||
"username": "astrbot",
|
||||
"password": "",
|
||||
"pbkdf2_password": "pbkdf2_sha256$600000$00$00",
|
||||
"password": existing_legacy_password,
|
||||
"pbkdf2_password": existing_pbkdf2_password,
|
||||
"password_storage_upgraded": True,
|
||||
"password_change_required": True,
|
||||
}
|
||||
},
|
||||
@@ -306,7 +314,7 @@ class TestAstrBotConfigLoad:
|
||||
)
|
||||
generated_password = getattr(config, "_generated_dashboard_password", None)
|
||||
|
||||
assert isinstance(generated_password, str)
|
||||
assert generated_password is None
|
||||
assert config["dashboard"]["password_change_required"] is True
|
||||
assert config["dashboard"]["password_storage_upgraded"] is True
|
||||
assert (
|
||||
@@ -314,12 +322,60 @@ class TestAstrBotConfigLoad:
|
||||
is True
|
||||
)
|
||||
assert verify_dashboard_password(
|
||||
config["dashboard"]["pbkdf2_password"], generated_password
|
||||
config["dashboard"]["pbkdf2_password"], existing_password
|
||||
)
|
||||
assert verify_dashboard_password(
|
||||
config["dashboard"]["password"], generated_password
|
||||
config["dashboard"]["password"], existing_password
|
||||
)
|
||||
|
||||
def test_reset_password_env_rotates_existing_password(
|
||||
self, temp_config_path, monkeypatch
|
||||
):
|
||||
"""Test that explicit reset rotates dashboard password on startup."""
|
||||
existing_password = "ExistingPass123"
|
||||
reset_password = "ResetPass123"
|
||||
monkeypatch.setenv("ASTRBOT_DASHBOARD_RESET_PASSWORD", "1")
|
||||
monkeypatch.setenv("ASTRBOT_DASHBOARD_INITIAL_PASSWORD", reset_password)
|
||||
default_config = {
|
||||
"dashboard": {
|
||||
"username": "astrbot",
|
||||
"password": "",
|
||||
"pbkdf2_password": "",
|
||||
"password_storage_upgraded": False,
|
||||
"password_change_required": False,
|
||||
},
|
||||
}
|
||||
with open(temp_config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"dashboard": {
|
||||
"username": "astrbot",
|
||||
"password": hash_legacy_dashboard_password(existing_password),
|
||||
"pbkdf2_password": hash_dashboard_password(existing_password),
|
||||
"password_storage_upgraded": True,
|
||||
"password_change_required": False,
|
||||
}
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
config = AstrBotConfig(
|
||||
config_path=temp_config_path,
|
||||
default_config=default_config,
|
||||
)
|
||||
|
||||
assert getattr(config, "_generated_dashboard_password", None) == reset_password
|
||||
assert verify_dashboard_password(
|
||||
config["dashboard"]["pbkdf2_password"], reset_password
|
||||
)
|
||||
assert verify_dashboard_password(config["dashboard"]["password"], reset_password)
|
||||
assert not verify_dashboard_password(
|
||||
config["dashboard"]["pbkdf2_password"], existing_password
|
||||
)
|
||||
assert config["dashboard"]["password_change_required"] is True
|
||||
assert config["dashboard"]["password_storage_upgraded"] is True
|
||||
assert os.environ["ASTRBOT_DASHBOARD_RESET_PASSWORD"] == "0"
|
||||
|
||||
def test_legacy_astrbot_user_without_change_flag_keeps_legacy_password(
|
||||
self, temp_config_path
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user