From abfbc99cd2b801ea9df3ed6fc62e48518b61d02b Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:53:23 +0800 Subject: [PATCH 1/2] fix: skip unavailable WebUI startup --- astrbot/core/initial_loader.py | 5 ++++ main.py | 1 + tests/test_main.py | 44 ++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/astrbot/core/initial_loader.py b/astrbot/core/initial_loader.py index 3f836a4c42..7947459237 100644 --- a/astrbot/core/initial_loader.py +++ b/astrbot/core/initial_loader.py @@ -22,6 +22,7 @@ def __init__(self, db: BaseDatabase, log_broker: LogBroker) -> None: self.logger = logger self.log_broker = log_broker self.webui_dir: str | None = None + self.webui_available = True async def start(self) -> None: core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db) @@ -35,6 +36,10 @@ async def start(self) -> None: core_task = core_lifecycle.start() + if not self.webui_available: + await core_task + return + webui_dir = self.webui_dir self.dashboard_server = AstrBotDashboard( diff --git a/main.py b/main.py index 9f62fdf410..3cc4138b79 100644 --- a/main.py +++ b/main.py @@ -130,6 +130,7 @@ async def main_async(webui_dir_arg: str | None) -> None: core_lifecycle = InitialLoader(db, log_broker) core_lifecycle.webui_dir = webui_dir + core_lifecycle.webui_available = webui_dir is not None await core_lifecycle.start() diff --git a/tests/test_main.py b/tests/test_main.py index ae60366315..46bb59fb00 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -257,3 +257,47 @@ async def test_check_dashboard_files_with_webui_dir_arg(monkeypatch): assert result == valid_dir mock_download.assert_not_called() mock_get_version.assert_not_called() + + +@pytest.mark.asyncio +async def test_main_async_disables_webui_when_dashboard_check_fails(): + with mock.patch( + "main.check_dashboard_files", + mock.AsyncMock(return_value=None), + ): + with mock.patch("main.log_broker", create=True): + with mock.patch("main.InitialLoader") as mock_loader: + mock_loader.return_value.start = mock.AsyncMock() + + from main import main_async + + await main_async(None) + + assert mock_loader.return_value.webui_dir is None + assert mock_loader.return_value.webui_available is False + mock_loader.return_value.start.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_initial_loader_skips_unavailable_webui(): + from astrbot.core.initial_loader import InitialLoader + + core_lifecycle = mock.MagicMock() + core_lifecycle.initialize = mock.AsyncMock() + core_lifecycle.start = mock.AsyncMock() + + with mock.patch( + "astrbot.core.initial_loader.AstrBotCoreLifecycle", + return_value=core_lifecycle, + ): + with mock.patch( + "astrbot.core.initial_loader.AstrBotDashboard", + ) as mock_dashboard: + loader = InitialLoader(mock.MagicMock(), mock.MagicMock()) + loader.webui_available = False + + await loader.start() + + core_lifecycle.initialize.assert_awaited_once() + core_lifecycle.start.assert_awaited_once() + mock_dashboard.assert_not_called() From 92335ce160f55a8e4ea66dcf09288ce99ae39372 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:58:17 +0800 Subject: [PATCH 2/2] fix: stop core on WebUI-disabled cancellation --- astrbot/core/initial_loader.py | 6 +++++- tests/test_main.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/astrbot/core/initial_loader.py b/astrbot/core/initial_loader.py index 7947459237..ebe02b8c45 100644 --- a/astrbot/core/initial_loader.py +++ b/astrbot/core/initial_loader.py @@ -37,7 +37,11 @@ async def start(self) -> None: core_task = core_lifecycle.start() if not self.webui_available: - await core_task + try: + await core_task + except asyncio.CancelledError: + logger.info("🌈 正在关闭 AstrBot...") + await core_lifecycle.stop() return webui_dir = self.webui_dir diff --git a/tests/test_main.py b/tests/test_main.py index 46bb59fb00..836d34343d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,3 +1,4 @@ +import asyncio import os import sys from pathlib import Path @@ -301,3 +302,24 @@ async def test_initial_loader_skips_unavailable_webui(): core_lifecycle.initialize.assert_awaited_once() core_lifecycle.start.assert_awaited_once() mock_dashboard.assert_not_called() + + +@pytest.mark.asyncio +async def test_initial_loader_stops_core_when_unavailable_webui_is_cancelled(): + from astrbot.core.initial_loader import InitialLoader + + core_lifecycle = mock.MagicMock() + core_lifecycle.initialize = mock.AsyncMock() + core_lifecycle.start = mock.AsyncMock(side_effect=asyncio.CancelledError) + core_lifecycle.stop = mock.AsyncMock() + + with mock.patch( + "astrbot.core.initial_loader.AstrBotCoreLifecycle", + return_value=core_lifecycle, + ): + loader = InitialLoader(mock.MagicMock(), mock.MagicMock()) + loader.webui_available = False + + await loader.start() + + core_lifecycle.stop.assert_awaited_once()