From 19864b3f8562eaa95ea75ec093fe301cfe49e81e Mon Sep 17 00:00:00 2001 From: Weilong Liao <37870767+Soulter@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:48:47 +0800 Subject: [PATCH] fix: recover interrupted dashboard upgrades --- astrbot/dashboard/services/update_service.py | 51 +++- dashboard/src/App.vue | 2 + dashboard/src/api/v1.ts | 116 ++++++++-- .../shared/UpgradeRecoveryDialog.vue | 217 ++++++++++++++++++ .../src/i18n/locales/en-US/core/common.json | 10 + .../src/i18n/locales/ru-RU/core/common.json | 12 +- .../src/i18n/locales/zh-CN/core/common.json | 10 + .../full/vertical-header/VerticalHeader.vue | 21 +- tests/test_dashboard.py | 66 ++++-- 9 files changed, 457 insertions(+), 48 deletions(-) create mode 100644 dashboard/src/components/shared/UpgradeRecoveryDialog.vue diff --git a/astrbot/dashboard/services/update_service.py b/astrbot/dashboard/services/update_service.py index 07a7e5c54..388812e70 100644 --- a/astrbot/dashboard/services/update_service.py +++ b/astrbot/dashboard/services/update_service.py @@ -92,6 +92,7 @@ class UpdateService: self.demo_mode = demo_mode self.clear_site_data_headers = clear_site_data_headers self.update_progress: dict[str, dict] = {} + self._update_tasks: dict[str, asyncio.Task] = {} def get_update_progress(self, progress_id: str) -> UpdateServiceResult: if not progress_id: @@ -156,7 +157,43 @@ class UpdateService: if proxy: proxy = proxy.removesuffix("/") + existing_task = self._update_tasks.get(progress_id) + if existing_task and not existing_task.done(): + return UpdateServiceResult( + data={"id": progress_id, "status": "running"}, + message="更新任务正在进行中。", + headers=self.clear_site_data_headers, + ) + self._init_update_progress(progress_id, version) + task = asyncio.create_task( + self._run_update_project(progress_id, version, latest, reboot, proxy) + ) + self._update_tasks[progress_id] = task + task.add_done_callback(lambda _task: self._update_tasks.pop(progress_id, None)) + return UpdateServiceResult( + data={"id": progress_id, "status": "running"}, + message="更新任务已开始。", + headers=self.clear_site_data_headers, + ) + + async def _run_update_project( + self, + progress_id: str, + version: str, + latest: bool, + reboot: bool, + proxy: str | None, + ) -> None: + """Run the long core update outside the request lifecycle. + + Args: + progress_id: Progress record id reported to the frontend. + version: Target version without the latest sentinel. + latest: Whether to install the latest release. + reboot: Whether to restart AstrBot after applying files. + proxy: Optional GitHub proxy URL. + """ update_temp_dir = Path(get_astrbot_system_tmp_path()) / "updates" update_temp_dir.mkdir(parents=True, exist_ok=True) update_token = uuid.uuid4().hex @@ -308,10 +345,16 @@ class UpdateService: "overall_percent": 100, }, ) - return UpdateServiceResult( - message=message, - headers=self.clear_site_data_headers, + logger.info(message) + except asyncio.CancelledError: + self.update_progress[progress_id].update( + { + "status": "error", + "message": "更新任务已取消。", + }, ) + logger.warning(f"Update task was cancelled: {progress_id}") + raise except Exception as exc: self.update_progress[progress_id].update( { @@ -320,7 +363,7 @@ class UpdateService: }, ) logger.error(f"/api/update_project: {traceback.format_exc()}") - raise UpdateServiceError(exc.__str__()) from exc + logger.debug(f"Update task failed: {exc!s}") finally: for zip_path in (dashboard_zip_path, core_zip_path): try: diff --git a/dashboard/src/App.vue b/dashboard/src/App.vue index af23e75ff..6ce7b460a 100644 --- a/dashboard/src/App.vue +++ b/dashboard/src/App.vue @@ -1,6 +1,7 @@