mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
chore: 更新项目配置和 CLI
- 更新 .env.example 环境变量示例 - 更新 pyproject.toml 依赖配置 - 删除 tui 相关命令 (cmd_tui.py, cmd_run_tui.py) - 更新 CLI i18n 和核心模块 - 删除 tombi.toml
This commit is contained in:
@@ -90,12 +90,6 @@ ASTRBOT_DASHBOARD_ENABLE=True
|
||||
# 默认 Default: zh (跟随系统 locale / follows system locale)
|
||||
# ASTRBOT_CLI_LANG=zh
|
||||
|
||||
# TUI 界面语言
|
||||
# TUI interface language
|
||||
# 可选值 Values: zh (中文), en (英文)
|
||||
# 默认 Default: zh
|
||||
# ASTRBOT_TUI_LANG=zh
|
||||
|
||||
# ------------------------------------------
|
||||
# 网络配置 / Network Configuration
|
||||
# ------------------------------------------
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -9,6 +10,18 @@ __all__ = ["logger"]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "cli":
|
||||
from .cli.__main__ import cli
|
||||
|
||||
return cli()
|
||||
if name == "cli_rs":
|
||||
from .rust._core import cli
|
||||
|
||||
def cli_rs_wrapper() -> None:
|
||||
return cli(sys.argv)
|
||||
|
||||
return cli_rs_wrapper
|
||||
|
||||
if name == "logger":
|
||||
from .core import logger
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import click
|
||||
from click.shell_completion import get_completion_class
|
||||
|
||||
from . import __version__
|
||||
from .commands import bk, conf, init, plug, run, tui, uninstall
|
||||
from .commands import bk, conf, init, plug, run, uninstall
|
||||
from .i18n import t
|
||||
|
||||
logo_tmpl = r"""
|
||||
@@ -85,7 +85,6 @@ cli.add_command(plug)
|
||||
cli.add_command(conf)
|
||||
cli.add_command(uninstall)
|
||||
cli.add_command(bk)
|
||||
cli.add_command(tui)
|
||||
|
||||
|
||||
@click.command()
|
||||
|
||||
@@ -3,7 +3,6 @@ from .cmd_conf import conf
|
||||
from .cmd_init import init
|
||||
from .cmd_plug import plug
|
||||
from .cmd_run import run
|
||||
from .cmd_tui import tui
|
||||
from .cmd_uninstall import uninstall
|
||||
|
||||
__all__ = ["bk", "conf", "init", "plug", "run", "tui", "uninstall"]
|
||||
__all__ = ["bk", "conf", "init", "plug", "run", "uninstall"]
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
"""AstrBot Run TUI - A beautiful textual interface for running AstrBot.
|
||||
|
||||
This module provides a Textual-based TUI for `astrbot run` with:
|
||||
- Animated ASCII logo
|
||||
- Live log viewer
|
||||
- Platform status indicators
|
||||
- Only activates in interactive TTY environments
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import typing
|
||||
from collections.abc import Awaitable, Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import Footer, Header, Log, Static
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from rich.console import Console
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
else:
|
||||
Console: Any = None
|
||||
Style: Any = None
|
||||
Text: Any = None
|
||||
|
||||
|
||||
# AstrBot ASCII Logo
|
||||
ASTRBOT_LOGO = r"""
|
||||
___ _______.___________..______ .______ ______ .___________.
|
||||
/ \ / | || _ \ | _ \ / __ \ | |
|
||||
/ ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----`
|
||||
/ /_\ \ \ \ | | | / | _ < | | | | | |
|
||||
/ _____ \ .----) | | | | |\ \----.| |_) | | `--' | | |
|
||||
/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__|
|
||||
"""
|
||||
|
||||
|
||||
class AstrBotRunTUI(App):
|
||||
"""Textual TUI for AstrBot run command."""
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
#logo-container {
|
||||
height: auto;
|
||||
padding: 1 2;
|
||||
background: $surface-darken-1;
|
||||
border: solid $primary;
|
||||
}
|
||||
|
||||
#logo-text {
|
||||
color: $primary;
|
||||
text-style: bold;
|
||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#log-section {
|
||||
border: solid $accent;
|
||||
height: 70%;
|
||||
margin: 1 2;
|
||||
}
|
||||
|
||||
#log-header {
|
||||
background: $accent-darken-1;
|
||||
padding: 1 2;
|
||||
color: $text;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
Log {
|
||||
background: $surface-darken-2;
|
||||
color: $text;
|
||||
border: solid $accent-darken-2;
|
||||
}
|
||||
|
||||
#status-section {
|
||||
height: auto;
|
||||
padding: 1 2;
|
||||
background: $surface-darken-1;
|
||||
border-top: solid $primary;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding: 0 2;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
color: $success;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS: typing.ClassVar[list[Binding]] = [
|
||||
Binding("q", "quit", "Quit", show=True),
|
||||
Binding("ctrl+c", "quit", "Quit", show=False),
|
||||
Binding("l", "toggle_logs", "Toggle Logs", show=True),
|
||||
]
|
||||
|
||||
log_visible = reactive(True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
startup_coro: Callable[[], Awaitable[Any]],
|
||||
astrbot_root: Path,
|
||||
backend_only: bool = False,
|
||||
host: str | None = None,
|
||||
port: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.startup_coro = startup_coro
|
||||
self.astrbot_root = astrbot_root
|
||||
self.backend_only = backend_only
|
||||
self.host = host
|
||||
self.port = port
|
||||
self._animation_frame = 0
|
||||
self._startup_done = False
|
||||
self._log_lines: list[str] = []
|
||||
self.console: Any = Console() if Console else None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create child widgets."""
|
||||
yield Header()
|
||||
|
||||
# Animated Logo
|
||||
with Container(id="logo-container"):
|
||||
yield Static(self._get_animated_logo(), id="logo-text")
|
||||
|
||||
# Main content
|
||||
with Vertical(id="main-container"):
|
||||
# Log viewer
|
||||
with Container(
|
||||
id="log-section", classes="" if self.log_visible else "hidden"
|
||||
):
|
||||
yield Static("📋 Live Logs", id="log-header")
|
||||
yield Log(id="log-viewer")
|
||||
|
||||
# Status bar
|
||||
with Horizontal(id="status-section"):
|
||||
yield Static("🌟 AstrBot", classes="status-item status-ok")
|
||||
yield Static(
|
||||
f"📁 {self.astrbot_root.name}",
|
||||
classes="status-item",
|
||||
id="root-status",
|
||||
)
|
||||
if not self.backend_only:
|
||||
dashboard_url = (
|
||||
f"http://{self.host or 'localhost'}:{self.port or '6185'}"
|
||||
)
|
||||
yield Static(
|
||||
f"🌐 Dashboard: [link]{dashboard_url}[/link]",
|
||||
classes="status-item",
|
||||
id="dashboard-status",
|
||||
)
|
||||
yield Static(
|
||||
"⚡ Running", classes="status-item status-ok", id="run-status"
|
||||
)
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when app is mounted."""
|
||||
self.title = "AstrBot"
|
||||
self.sub_title = "AI Chatbot Framework"
|
||||
|
||||
# Start the startup coroutine
|
||||
self.set_timer(0.1, self._run_startup)
|
||||
|
||||
# Animate logo
|
||||
self.set_interval(0.5, self._animate_logo)
|
||||
|
||||
# Get the log widget and configure it
|
||||
log_widget = self.query_one("#log-viewer", Log)
|
||||
log_widget.write_line("🚀 AstrBot TUI initialized")
|
||||
log_widget.write_line(f"📁 Running from: {self.astrbot_root}")
|
||||
if not self.backend_only:
|
||||
log_widget.write_line(
|
||||
f"🌐 Dashboard will be available at: {self.host or 'localhost'}:{self.port or '6185'}"
|
||||
)
|
||||
log_widget.write_line("")
|
||||
|
||||
def _get_animated_logo(self) -> str:
|
||||
"""Get the logo with optional animation effect."""
|
||||
lines = ASTRBOT_LOGO.strip().split("\n")
|
||||
|
||||
if self.console and hasattr(self, "_animation_frame"):
|
||||
# Create animated version with color cycling
|
||||
frame = self._animation_frame % 4
|
||||
colors = ["#00D9FF", "#00FF87", "#FFD700", "#FF6B6B"]
|
||||
color = colors[frame]
|
||||
|
||||
text = Text()
|
||||
for i, line in enumerate(lines):
|
||||
style = Style(color=color, bold=True) if i == 0 else Style(color=color)
|
||||
text.append(line + "\n", style=style)
|
||||
return str(text)
|
||||
|
||||
return ASTRBOT_LOGO
|
||||
|
||||
def _animate_logo(self) -> None:
|
||||
"""Update the animated logo."""
|
||||
self._animation_frame = (self._animation_frame + 1) % 4
|
||||
logo_widget = self.query_one("#logo-text", Static)
|
||||
logo_widget.update(self._get_animated_logo())
|
||||
|
||||
async def _run_startup(self) -> None:
|
||||
"""Run the AstrBot startup coroutine."""
|
||||
if self._startup_done:
|
||||
return
|
||||
self._startup_done = True
|
||||
|
||||
try:
|
||||
log_widget = self.query_one("#log-viewer", Log)
|
||||
log_widget.write_line("⏳ Initializing AstrBot...")
|
||||
|
||||
await self.startup_coro()
|
||||
|
||||
log_widget.write_line("")
|
||||
log_widget.write_line("✅ AstrBot started successfully!")
|
||||
except Exception as e:
|
||||
log_widget = self.query_one("#log-viewer", Log)
|
||||
log_widget.write_line(f"❌ Error during startup: {e}")
|
||||
log_widget.write_line("Check logs for details.")
|
||||
|
||||
def action_toggle_logs(self) -> None:
|
||||
"""Toggle log visibility."""
|
||||
self.log_visible = not self.log_visible
|
||||
log_section = self.query_one("#log-section", Container)
|
||||
if self.log_visible:
|
||||
log_section.remove_class("hidden")
|
||||
else:
|
||||
log_section.add_class("hidden")
|
||||
|
||||
async def action_quit(self) -> None:
|
||||
"""Quit the application."""
|
||||
self.exit()
|
||||
|
||||
def write_log(self, message: str) -> None:
|
||||
"""Write a message to the log viewer (can be called from outside)."""
|
||||
log_widget = self.query_one("#log-viewer", Log)
|
||||
log_widget.write_line(message)
|
||||
|
||||
|
||||
def is_interactive_tty() -> bool:
|
||||
"""Check if we're running in an interactive TTY."""
|
||||
return sys.stdin.isatty() and sys.stdout.isatty()
|
||||
|
||||
|
||||
async def run_tui(
|
||||
startup_coro: Callable[[], Awaitable[Any]],
|
||||
astrbot_root: Path,
|
||||
backend_only: bool = False,
|
||||
host: str | None = None,
|
||||
port: str | None = None,
|
||||
) -> None:
|
||||
"""Run the AstrBot TUI.
|
||||
|
||||
Args:
|
||||
startup_coro: Coroutine to run on startup
|
||||
astrbot_root: AstrBot root directory
|
||||
backend_only: Whether backend-only mode is enabled
|
||||
host: Dashboard host
|
||||
port: Dashboard port
|
||||
"""
|
||||
if not is_interactive_tty():
|
||||
# Not interactive, run without TUI
|
||||
await startup_coro()
|
||||
return
|
||||
|
||||
app = AstrBotRunTUI(
|
||||
startup_coro=startup_coro,
|
||||
astrbot_root=astrbot_root,
|
||||
backend_only=backend_only,
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
|
||||
try:
|
||||
await app.run_async()
|
||||
except Exception:
|
||||
# Fallback to non-TUI mode
|
||||
await startup_coro()
|
||||
@@ -1,68 +0,0 @@
|
||||
"""TUI CLI command for AstrBot."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@click.command(name="tui")
|
||||
@click.option(
|
||||
"--debug",
|
||||
is_flag=True,
|
||||
help="Enable debug mode with verbose output.",
|
||||
)
|
||||
@click.option(
|
||||
"--host",
|
||||
default="http://localhost:6185",
|
||||
help="AstrBot dashboard host URL.",
|
||||
)
|
||||
@click.option(
|
||||
"--api-key",
|
||||
default=None,
|
||||
help="API key for authentication (optional, uses login if not provided).",
|
||||
)
|
||||
@click.option(
|
||||
"--username",
|
||||
default="astrbot",
|
||||
help="Username for login (if api-key not provided).",
|
||||
)
|
||||
@click.option(
|
||||
"--password",
|
||||
default="astrbot",
|
||||
help="Password for login (if api-key not provided).",
|
||||
)
|
||||
def tui(
|
||||
debug: bool,
|
||||
host: str,
|
||||
api_key: str | None,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
"""
|
||||
Launch the AstrBot Terminal User Interface (TUI).
|
||||
|
||||
This command starts an interactive terminal-based interface for AstrBot.
|
||||
The TUI connects to a running AstrBot instance via the dashboard API.
|
||||
"""
|
||||
try:
|
||||
from astrbot.cli.commands.tui_async import run_tui_async
|
||||
|
||||
run_tui_async(
|
||||
debug=debug,
|
||||
host=host,
|
||||
api_key=api_key,
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
except ImportError as e:
|
||||
click.echo(f"Error: Failed to import TUI module: {e}", err=True)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
click.echo(f"Error: Failed to start TUI: {e}", err=True)
|
||||
if debug:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -91,10 +91,6 @@ _TRANSLATIONS: dict[Language, dict[str, str]] = {
|
||||
"run_backend_only": "以无界面模式启动",
|
||||
"run_failed": "启动失败: {error}",
|
||||
"run_stopped": "AstrBot 已停止",
|
||||
# TUI command
|
||||
"tui_starting": "正在启动 TUI...",
|
||||
"tui_started": "TUI 已启动",
|
||||
"tui_failed": "TUI 启动失败: {error}",
|
||||
# Common
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
@@ -182,10 +178,6 @@ _TRANSLATIONS: dict[Language, dict[str, str]] = {
|
||||
"run_backend_only": "Starting in backend-only mode",
|
||||
"run_failed": "Failed to start: {error}",
|
||||
"run_stopped": "AstrBot stopped",
|
||||
# TUI command
|
||||
"tui_starting": "Starting TUI...",
|
||||
"tui_started": "TUI started",
|
||||
"tui_failed": "Failed to start TUI: {error}",
|
||||
# Common
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
|
||||
@@ -1268,7 +1268,7 @@ async def build_main_agent(
|
||||
# Internal tools (source='internal') bypass this check — they are
|
||||
# not user-togglable in the WebUI, so legacy entries must not block them.
|
||||
_inactivated: set[str] = set(
|
||||
sp.get("inactivated_llm_tools", [], scope="global", scope_id="global")
|
||||
str(sp.get("inactivated_llm_tools", [], scope="global", scope_id="global"))
|
||||
)
|
||||
for _tp in config.tool_providers:
|
||||
_tp_tools = _tp.get_tools(_provider_ctx)
|
||||
|
||||
@@ -115,6 +115,9 @@ ignore = [
|
||||
"E501",
|
||||
]
|
||||
|
||||
[tool.ty]
|
||||
exclude = ["tests", "tests/**", "**/tests/**"]
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "basic"
|
||||
pythonVersion = "3.10"
|
||||
|
||||
@@ -59,8 +59,8 @@ class TestSplitUmo:
|
||||
|
||||
def test_non_string_returns_none(self):
|
||||
"""UMO that is not a string returns None."""
|
||||
assert UmopConfigRouter._split_umo(None) is None
|
||||
assert UmopConfigRouter._split_umo(123) is None
|
||||
assert UmopConfigRouter._split_umo(None) is None # type: ignore 故意这样测试的
|
||||
assert UmopConfigRouter._split_umo(123) is None # type: ignore 故意这样测试的
|
||||
|
||||
def test_four_parts_returns_three(self):
|
||||
"""UMO with four parts splits to three (last keeps colon)."""
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[schema]
|
||||
strict = false
|
||||
Reference in New Issue
Block a user