diff --git a/.env.example b/.env.example index ac209b29c..c28f34b04 100644 --- a/.env.example +++ b/.env.example @@ -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 # ------------------------------------------ diff --git a/astrbot/__init__.py b/astrbot/__init__.py index f7604c5b1..e79dc4ffa 100644 --- a/astrbot/__init__.py +++ b/astrbot/__init__.py @@ -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 diff --git a/astrbot/cli/__main__.py b/astrbot/cli/__main__.py index e150729da..9245226fe 100644 --- a/astrbot/cli/__main__.py +++ b/astrbot/cli/__main__.py @@ -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() diff --git a/astrbot/cli/commands/__init__.py b/astrbot/cli/commands/__init__.py index 4e3671967..c5d5944bb 100644 --- a/astrbot/cli/commands/__init__.py +++ b/astrbot/cli/commands/__init__.py @@ -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"] diff --git a/astrbot/cli/commands/cmd_run_tui.py b/astrbot/cli/commands/cmd_run_tui.py deleted file mode 100644 index 092a0999f..000000000 --- a/astrbot/cli/commands/cmd_run_tui.py +++ /dev/null @@ -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() diff --git a/astrbot/cli/commands/cmd_tui.py b/astrbot/cli/commands/cmd_tui.py deleted file mode 100644 index 218d12aea..000000000 --- a/astrbot/cli/commands/cmd_tui.py +++ /dev/null @@ -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) diff --git a/astrbot/cli/i18n.py b/astrbot/cli/i18n.py index d685ff0a1..07c58e40d 100644 --- a/astrbot/cli/i18n.py +++ b/astrbot/cli/i18n.py @@ -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", diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index a341d8540..f671fb74c 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index b134349c7..29f29654f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,6 +115,9 @@ ignore = [ "E501", ] +[tool.ty] +exclude = ["tests", "tests/**", "**/tests/**"] + [tool.pyright] typeCheckingMode = "basic" pythonVersion = "3.10" diff --git a/tests/unit/test_umop_config_router.py b/tests/unit/test_umop_config_router.py index 578aaf665..43567deba 100644 --- a/tests/unit/test_umop_config_router.py +++ b/tests/unit/test_umop_config_router.py @@ -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).""" diff --git a/tombi.toml b/tombi.toml deleted file mode 100644 index 0e0673745..000000000 --- a/tombi.toml +++ /dev/null @@ -1,2 +0,0 @@ -[schema] -strict = false