From 8fa8c14b0bad245db29659ad60d7269ebb20bd7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A8=E3=82=A4=E3=82=AB=E3=82=AF?= <62183434+zouyonghe@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:21:04 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dapp=E5=86=85=E9=87=8D?= =?UTF-8?q?=E5=90=AF=E5=BC=82=E5=B8=B8=EF=BC=8C=E4=BF=AE=E5=A4=8Dapp?= =?UTF-8?q?=E5=86=85=E7=82=B9=E5=87=BB=E9=87=8D=E5=90=AF=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E7=AB=8B=E5=88=BB=E6=8F=90=E7=A4=BA=E9=87=8D=E5=90=AF=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E5=9C=A8=E5=90=8E=E7=AB=AF=E5=B0=B1=E7=BB=AA?= =?UTF-8?q?=E6=97=B6=E5=8F=8A=E6=97=B6=E5=88=B7=E6=96=B0=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=20(#5013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: patch pip distlib finder for frozen electron runtime * fix: use certifi CA bundle for runtime SSL requests * fix: configure certifi CA before core imports * fix: improve mac font fallback for dashboard text * fix: harden frozen pip patch and unify TLS connector * refactor: centralize dashboard CJK font fallback stacks * perf: reuse TLS context and avoid repeated frozen pip patch * refactor: bootstrap TLS setup before core imports * fix: use async confirm dialog for provider deletions * fix: replace native confirm dialogs in dashboard - Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback. - Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper. - Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron. * fix: capture runtime bootstrap logs after logger init - Add bootstrap record buffer in runtime_bootstrap for early TLS patch logs before logger is ready. - Flush buffered bootstrap logs to astrbot logger at process startup in main.py. - Include concrete exception details for TLS bootstrap failures to improve diagnosis. * fix: harden runtime bootstrap and unify confirm handling - Simplify bootstrap log buffering and add a public initialize hook for non-main startup paths. - Guard aiohttp TLS patching with feature/type checks and keep graceful fallback when internals are unavailable. - Standardize dashboard confirmation flow via shared confirm helpers across composition and options API components. * refactor: simplify runtime tls bootstrap and tighten confirm typing * refactor: align ssl helper namespace and confirm usage * fix: avoid frozen restart crash from multiprocessing import * fix: include missing frozen dependencies for windows backend * fix: use execv for stable backend reboot args * Revert "fix: use execv for stable backend reboot args" This reverts commit 9cc27becffeba0e117fea26aa5c2e1fe7afc6e36. * Revert "fix: include missing frozen dependencies for windows backend" This reverts commit 52554bea1fa61045451600c64447b7bf38cf6c92. * Revert "fix: avoid frozen restart crash from multiprocessing import" This reverts commit 10548645b0ba1e19b64194878ece478a48067959. * fix: reset pyinstaller onefile env before reboot * fix: unify electron restart path and tray-exit backend cleanup * fix: stabilize desktop restart detection and frozen reboot args * fix: make dashboard restart wait detection robust * fix: revert dashboard restart waiting interaction tweaks * fix: pass auth token for desktop graceful restart * fix: avoid false failure during graceful restart wait * fix: start restart waiting before electron restart call * fix: harden restart waiting and reboot arg parsing * fix: parse start_time as numeric timestamp * fix: preserve windows frozen reboot argv quoting * fix: align restart waiting with electron restart timing * fix: tighten graceful restart and unmanaged kill safety --- astrbot/core/updator.py | 11 +++- .../components/shared/WaitingForRestart.vue | 16 +++-- dashboard/src/utils/restartAstrBot.ts | 26 ++++++-- desktop/lib/backend-manager.js | 59 ++++++++++++++++--- 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index 94cfebeb1..e7c2aa54b 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -102,6 +102,15 @@ class AstrBotUpdator(RepoZipUpdator): return [executable, *args] return [executable, *sys.argv] + @staticmethod + def _exec_reboot(executable: str, argv: list[str]) -> None: + if os.name == "nt" and getattr(sys, "frozen", False): + quoted_executable = f'"{executable}"' if " " in executable else executable + quoted_args = [f'"{arg}"' if " " in arg else arg for arg in argv[1:]] + os.execl(executable, quoted_executable, *quoted_args) + return + os.execv(executable, argv) + def _reboot(self, delay: int = 3) -> None: """重启当前程序 在指定的延迟后,终止所有子进程并重新启动程序 @@ -114,7 +123,7 @@ class AstrBotUpdator(RepoZipUpdator): try: self._reset_pyinstaller_environment() reboot_argv = self._build_reboot_argv(executable) - os.execv(executable, reboot_argv) + self._exec_reboot(executable, reboot_argv) except Exception as e: logger.error(f"重启失败({executable}, {e}),请尝试手动重启。") raise e diff --git a/dashboard/src/components/shared/WaitingForRestart.vue b/dashboard/src/components/shared/WaitingForRestart.vue index ce97eebbd..41ce9ece2 100644 --- a/dashboard/src/components/shared/WaitingForRestart.vue +++ b/dashboard/src/components/shared/WaitingForRestart.vue @@ -31,16 +31,20 @@ export default { } }, methods: { - async check() { + async check(initialStartTime = null) { this.newStartTime = -1 this.cnt = 0 this.visible = true this.status = "" - const commonStore = useCommonStore() - try { - this.startTime = await commonStore.fetchStartTime() - } catch (_error) { - this.startTime = commonStore.getStartTime() + if (typeof initialStartTime === 'number' && Number.isFinite(initialStartTime)) { + this.startTime = initialStartTime + } else { + const commonStore = useCommonStore() + try { + this.startTime = await commonStore.fetchStartTime() + } catch (_error) { + this.startTime = commonStore.getStartTime() + } } console.log('start wfr') setTimeout(() => { diff --git a/dashboard/src/utils/restartAstrBot.ts b/dashboard/src/utils/restartAstrBot.ts index 2b32c3b81..8fb0b188d 100644 --- a/dashboard/src/utils/restartAstrBot.ts +++ b/dashboard/src/utils/restartAstrBot.ts @@ -1,13 +1,27 @@ import axios from 'axios' type WaitingForRestartRef = { - check: () => void | Promise + check: (initialStartTime?: number | null) => void | Promise stop?: () => void } -async function triggerWaiting(waitingRef?: WaitingForRestartRef | null) { +async function triggerWaiting( + waitingRef?: WaitingForRestartRef | null, + initialStartTime?: number | null +) { if (!waitingRef) return - await waitingRef.check() + await waitingRef.check(initialStartTime) +} + +async function fetchCurrentStartTime(): Promise { + try { + const response = await axios.get('/api/stat/start-time', { timeout: 1500 }) + const rawStartTime = response?.data?.data?.start_time + const numericStartTime = Number(rawStartTime) + return Number.isFinite(numericStartTime) ? numericStartTime : null + } catch (_error) { + return null + } } export async function restartAstrBot( @@ -17,13 +31,15 @@ export async function restartAstrBot( if (desktopBridge?.isElectron) { const authToken = localStorage.getItem('token') + const initialStartTime = await fetchCurrentStartTime() try { - const result = await desktopBridge.restartBackend(authToken) + const restartPromise = desktopBridge.restartBackend(authToken) + await triggerWaiting(waitingRef, initialStartTime) + const result = await restartPromise if (!result.ok) { waitingRef?.stop?.() throw new Error(result.reason || 'Failed to restart backend.') } - await triggerWaiting(waitingRef) } catch (error) { waitingRef?.stop?.() throw error diff --git a/desktop/lib/backend-manager.js b/desktop/lib/backend-manager.js index cc94f50ac..477995027 100644 --- a/desktop/lib/backend-manager.js +++ b/desktop/lib/backend-manager.js @@ -317,13 +317,6 @@ class BackendManager { if (sawBackendDown && previousStartTime === null) { return { ok: true, reason: null }; } - if ( - sawBackendDown && - previousStartTime !== null && - currentStartTime === null - ) { - return { ok: true, reason: null }; - } } if (Date.now() - start >= gracefulWaitMs) { @@ -566,6 +559,40 @@ class BackendManager { return Array.from(pids); } + getWindowsProcessInfo(pid) { + const result = spawnSync( + 'tasklist', + ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], + { + stdio: ['ignore', 'pipe', 'ignore'], + encoding: 'utf8', + windowsHide: true, + }, + ); + if (result.status !== 0 || !result.stdout) { + return null; + } + + const firstLine = result.stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .find((line) => line.length > 0); + if (!firstLine || firstLine.startsWith('INFO:')) { + return null; + } + + const fields = firstLine + .replace(/^"/, '') + .replace(/"$/, '') + .split('","'); + const imageName = fields[0] || ''; + const parsedPid = Number.parseInt(fields[1] || '', 10); + if (!imageName || !Number.isInteger(parsedPid) || parsedPid !== Number(pid)) { + return null; + } + return { imageName, pid: parsedPid }; + } + async stopUnmanagedBackendByPort() { if (!this.app.isPackaged || process.platform !== 'win32') { return false; @@ -585,7 +612,25 @@ class BackendManager { `Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`, ); + const expectedImageName = ( + path.basename(this.getPackagedBackendPath() || '') || 'astrbot-backend.exe' + ).toLowerCase(); + for (const pid of pids) { + const processInfo = this.getWindowsProcessInfo(pid); + if (!processInfo) { + this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`); + continue; + } + + const actualImageName = processInfo.imageName.toLowerCase(); + if (actualImageName !== expectedImageName) { + this.log( + `Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`, + ); + continue; + } + try { // Synchronous taskkill is acceptable here because unmanaged cleanup // is performed only during shutdown/restart control flows.