fix: 修复app内重启异常,修复app内点击重启不能立刻提示重启,以及在后端就绪时及时刷新界面的问题 (#5013)

* 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 9cc27becff.

* Revert "fix: include missing frozen dependencies for windows backend"

This reverts commit 52554bea1f.

* Revert "fix: avoid frozen restart crash from multiprocessing import"

This reverts commit 10548645b0.

* 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
This commit is contained in:
エイカク
2026-02-10 22:21:04 +09:00
committed by GitHub
parent 64de474139
commit 8fa8c14b0b
4 changed files with 93 additions and 19 deletions
+10 -1
View File
@@ -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
@@ -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(() => {
+21 -5
View File
@@ -1,13 +1,27 @@
import axios from 'axios'
type WaitingForRestartRef = {
check: () => void | Promise<void>
check: (initialStartTime?: number | null) => void | Promise<void>
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<number | null> {
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
+52 -7
View File
@@ -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.