From 99c66c24109835e929e8d5b0937a8a8edff34f75 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 23 Mar 2026 17:21:05 +0800 Subject: [PATCH] feat(cli): add real-time log streaming in non-interactive mode - Non-interactive mode now streams logs to stdout with color-coded levels - Add proper async cleanup when shutting down - Fix type annotations in coze and deerflow agent runners --- astrbot/cli/commands/cmd_run.py | 67 +++++++++++++++++-- .../agent/runners/coze/coze_agent_runner.py | 2 +- .../runners/deerflow/deerflow_agent_runner.py | 2 +- astrbot/core/computer/tools/browser.py | 6 +- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index fe8a1fca4..0d6ea75fa 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -323,12 +323,69 @@ def run( ) ) else: - click.echo("AstrBot is running...") - if backend_only: - click.echo("Visit the dashboard at : https://dash.astrbot.men/") - click.echo("Backend Requests : localhost or based on https") + # Non-interactive mode: run with stdout log streaming + async def run_with_logging() -> None: + from astrbot.core import LogBroker, LogManager, db_helper, logger + from astrbot.core.initial_loader import InitialLoader - asyncio.run(run_astrbot(astrbot_root)) + if ( + os.environ.get("ASTRBOT_DASHBOARD_ENABLE", os.environ.get("DASHBOARD_ENABLE")) + == "True" + ): + await DashboardManager().ensure_installed(astrbot_root) + + log_broker = LogBroker() + LogManager.set_queue_handler(logger, log_broker) + + # Register a stdout subscriber for real-time log streaming + log_queue = log_broker.register() + + db = db_helper + initial_loader = InitialLoader(db, log_broker) + + # Start a task to stream logs to stdout + async def stream_logs() -> None: + """Stream logs from LogBroker to stdout.""" + import logging + while True: + try: + log_entry = await asyncio.wait_for(log_queue.get(), timeout=0.5) + # Format: [LEVEL] message + level = log_entry.get("level_name", "INFO") + message = log_entry.get("message", "") + if message: + level_color = { + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red", + }.get(level, "white") + click.secho(f"[{level}]", fg=level_color, bold=False, nl=False) + click.echo(f" {message}") + except asyncio.TimeoutError: + continue + except asyncio.CancelledError: + break + + # Start streaming task + stream_task = asyncio.create_task(stream_logs()) + + try: + await initial_loader.start() + finally: + stream_task.cancel() + try: + await stream_task + except asyncio.CancelledError: + pass + + click.echo("AstrBot is running... (streaming logs)") + if backend_only: + click.echo("Dashboard: https://dash.astrbot.men/") + click.echo("Backend: localhost or based on https") + + asyncio.run(run_with_logging()) except KeyboardInterrupt: click.echo("AstrBot has been shut down.") except Timeout: diff --git a/astrbot/core/agent/runners/coze/coze_agent_runner.py b/astrbot/core/agent/runners/coze/coze_agent_runner.py index 171b34112..93c1f8970 100644 --- a/astrbot/core/agent/runners/coze/coze_agent_runner.py +++ b/astrbot/core/agent/runners/coze/coze_agent_runner.py @@ -86,7 +86,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): self.file_id_cache: dict[str, dict[str, str]] = {} @override - async def step(self): + async def step(self) -> AsyncGenerator[AgentResponse, None]: """ 执行 Coze Agent 的一个步骤 """ diff --git a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py index 66692aa01..0ca4cfe6f 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py @@ -23,8 +23,8 @@ from astrbot.core.utils.config_number import coerce_int_config from ...hooks import BaseAgentRunHooks from ...response import AgentResponseData from ...run_context import ContextWrapper, TContext +from ...tool_executor import BaseFunctionToolExecutor from ..base import AgentResponse, AgentState, BaseAgentRunner -from ..tool_executor import BaseFunctionToolExecutor from .constants import DEERFLOW_SESSION_PREFIX, DEERFLOW_THREAD_ID_KEY from .deerflow_api_client import DeerFlowAPIClient from .deerflow_content_mapper import ( diff --git a/astrbot/core/computer/tools/browser.py b/astrbot/core/computer/tools/browser.py index b8ad9c1ee..0392ed0e3 100644 --- a/astrbot/core/computer/tools/browser.py +++ b/astrbot/core/computer/tools/browser.py @@ -70,7 +70,7 @@ class BrowserExecTool(FunctionTool): async def call( self, context: ContextWrapper[AstrAgentContext], - cmd: str, + cmd: str = "", timeout: int = 30, description: str | None = None, tags: str | None = None, @@ -133,7 +133,7 @@ class BrowserBatchExecTool(FunctionTool): async def call( self, context: ContextWrapper[AstrAgentContext], - commands: list[str], + commands: list[str] | None = None, timeout: int = 60, stop_on_error: bool = True, description: str | None = None, @@ -182,7 +182,7 @@ class RunBrowserSkillTool(FunctionTool): async def call( self, context: ContextWrapper[AstrAgentContext], - skill_key: str, + skill_key: str = "", timeout: int = 60, stop_on_error: bool = True, include_trace: bool = False,