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 commit9cc27becff. * Revert "fix: include missing frozen dependencies for windows backend" This reverts commit52554bea1f. * Revert "fix: avoid frozen restart crash from multiprocessing import" This reverts commit10548645b0. * 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:
+10
-1
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user