diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index 87268c609..94cfebeb1 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -44,6 +44,64 @@ class AstrBotUpdator(RepoZipUpdator): except psutil.NoSuchProcess: pass + @staticmethod + def _is_option_arg(arg: str) -> bool: + return arg.startswith("-") + + @classmethod + def _collect_flag_values(cls, argv: list[str], flag: str) -> str | None: + try: + idx = argv.index(flag) + except ValueError: + return None + + if idx + 1 >= len(argv): + return None + + value_parts: list[str] = [] + for arg in argv[idx + 1 :]: + if cls._is_option_arg(arg): + break + if arg: + value_parts.append(arg) + + if not value_parts: + return None + + return " ".join(value_parts).strip() or None + + @classmethod + def _resolve_webui_dir_arg(cls, argv: list[str]) -> str | None: + return cls._collect_flag_values(argv, "--webui-dir") + + def _build_frozen_reboot_args(self) -> list[str]: + argv = list(sys.argv[1:]) + webui_dir = self._resolve_webui_dir_arg(argv) + if not webui_dir: + webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR") + + if webui_dir: + return ["--webui-dir", webui_dir] + return [] + + @staticmethod + def _reset_pyinstaller_environment() -> None: + if not getattr(sys, "frozen", False): + return + os.environ["PYINSTALLER_RESET_ENVIRONMENT"] = "1" + for key in list(os.environ.keys()): + if key.startswith("_PYI_"): + os.environ.pop(key, None) + + def _build_reboot_argv(self, executable: str) -> list[str]: + if os.environ.get("ASTRBOT_CLI") == "1": + args = sys.argv[1:] + return [executable, "-m", "astrbot.cli.__main__", *args] + if getattr(sys, "frozen", False): + args = self._build_frozen_reboot_args() + return [executable, *args] + return [executable, *sys.argv] + def _reboot(self, delay: int = 3) -> None: """重启当前程序 在指定的延迟后,终止所有子进程并重新启动程序 @@ -51,28 +109,14 @@ class AstrBotUpdator(RepoZipUpdator): """ time.sleep(delay) self.terminate_child_processes() - if os.name == "nt": - py = f'"{sys.executable}"' - else: - py = sys.executable + executable = sys.executable try: - # 仅 CLI 模式走 `python -m astrbot.cli.__main__`, - # 打包后的后端可执行文件需要直接 exec 自身。 - if os.environ.get("ASTRBOT_CLI") == "1": - if os.name == "nt": - args = [f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]] - else: - args = sys.argv[1:] - os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args) - else: - if getattr(sys, "frozen", False): - # Frozen executable should not receive argv[0] as a positional argument. - os.execl(sys.executable, py, *sys.argv[1:]) - else: - os.execl(sys.executable, py, *sys.argv) + self._reset_pyinstaller_environment() + reboot_argv = self._build_reboot_argv(executable) + os.execv(executable, reboot_argv) except Exception as e: - logger.error(f"重启失败({py}, {e}),请尝试手动重启。") + logger.error(f"重启失败({executable}, {e}),请尝试手动重启。") raise e async def check_update( diff --git a/dashboard/src/components/shared/BackupDialog.vue b/dashboard/src/components/shared/BackupDialog.vue index cd23b691e..c69aa4464 100644 --- a/dashboard/src/components/shared/BackupDialog.vue +++ b/dashboard/src/components/shared/BackupDialog.vue @@ -371,6 +371,7 @@ import { ref, computed, watch } from 'vue' import axios from 'axios' import { useI18n } from '@/i18n/composables' import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog' +import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot' import WaitingForRestart from './WaitingForRestart.vue' const { t } = useI18n() @@ -948,12 +949,12 @@ const formatISODate = (isoString) => { } // 重启 AstrBot -const restartAstrBot = () => { - axios.post('/api/stat/restart-core').then(() => { - if (wfr.value) { - wfr.value.check() - } - }) +const restartAstrBot = async () => { + try { + await restartAstrBotRuntime(wfr.value) + } catch (error) { + console.error(error) + } } // 重置所有状态 diff --git a/dashboard/src/components/shared/MigrationDialog.vue b/dashboard/src/components/shared/MigrationDialog.vue index 445200b71..9716d8d1a 100644 --- a/dashboard/src/components/shared/MigrationDialog.vue +++ b/dashboard/src/components/shared/MigrationDialog.vue @@ -113,6 +113,7 @@ import { ref, computed, watch } from 'vue' import axios from 'axios' import { useI18n } from '@/i18n/composables' +import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot' import ConsoleDisplayer from './ConsoleDisplayer.vue' import WaitingForRestart from './WaitingForRestart.vue' @@ -258,12 +259,12 @@ const getPlatformLabel = (platform) => { } // 重启 AstrBot -const restartAstrBot = () => { - axios.post('/api/stat/restart-core').then(() => { - if (wfr.value) { - wfr.value.check(); - } - }) +const restartAstrBot = async () => { + try { + await restartAstrBotRuntime(wfr.value) + } catch (error) { + console.error(error) + } } // 打开对话框的方法 diff --git a/dashboard/src/components/shared/WaitingForRestart.vue b/dashboard/src/components/shared/WaitingForRestart.vue index 2fbda30b2..ce97eebbd 100644 --- a/dashboard/src/components/shared/WaitingForRestart.vue +++ b/dashboard/src/components/shared/WaitingForRestart.vue @@ -47,6 +47,11 @@ export default { this.timeoutInternal() }, 1000) }, + stop() { + this.visible = false + this.cnt = 0 + this.newStartTime = -1 + }, timeoutInternal() { console.log('wfr: timeoutInternal', this.newStartTime, this.startTime) if (this.newStartTime === -1 && this.cnt < 60 && this.visible) { diff --git a/dashboard/src/types/electron-bridge.d.ts b/dashboard/src/types/electron-bridge.d.ts index 9bcc18006..07676cdfc 100644 --- a/dashboard/src/types/electron-bridge.d.ts +++ b/dashboard/src/types/electron-bridge.d.ts @@ -11,7 +11,7 @@ declare global { restarting: boolean; canManage: boolean; }>; - restartBackend: () => Promise<{ + restartBackend: (authToken?: string | null) => Promise<{ ok: boolean; reason: string | null; }>; diff --git a/dashboard/src/utils/restartAstrBot.ts b/dashboard/src/utils/restartAstrBot.ts new file mode 100644 index 000000000..2b32c3b81 --- /dev/null +++ b/dashboard/src/utils/restartAstrBot.ts @@ -0,0 +1,36 @@ +import axios from 'axios' + +type WaitingForRestartRef = { + check: () => void | Promise + stop?: () => void +} + +async function triggerWaiting(waitingRef?: WaitingForRestartRef | null) { + if (!waitingRef) return + await waitingRef.check() +} + +export async function restartAstrBot( + waitingRef?: WaitingForRestartRef | null +): Promise { + const desktopBridge = window.astrbotDesktop + + if (desktopBridge?.isElectron) { + const authToken = localStorage.getItem('token') + try { + const result = await desktopBridge.restartBackend(authToken) + if (!result.ok) { + waitingRef?.stop?.() + throw new Error(result.reason || 'Failed to restart backend.') + } + await triggerWaiting(waitingRef) + } catch (error) { + waitingRef?.stop?.() + throw error + } + return + } + + await axios.post('/api/stat/restart-core') + await triggerWaiting(waitingRef) +} diff --git a/dashboard/src/views/ConfigPage.vue b/dashboard/src/views/ConfigPage.vue index 4b129d67b..4c27081f3 100644 --- a/dashboard/src/views/ConfigPage.vue +++ b/dashboard/src/views/ConfigPage.vue @@ -190,6 +190,7 @@ import WaitingForRestart from '@/components/shared/WaitingForRestart.vue'; import StandaloneChat from '@/components/chat/StandaloneChat.vue'; import { VueMonacoEditor } from '@guolao/vue-monaco-editor' import { useI18n, useModuleI18n } from '@/i18n/composables'; +import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot'; import { askForConfirmation as askForConfirmationDialog, useConfirmDialog @@ -375,9 +376,7 @@ export default { this.save_message_success = "success"; if (this.isSystemConfig) { - axios.post('/api/stat/restart-core').then(() => { - this.$refs.wfr.check(); - }) + restartAstrBotRuntime(this.$refs.wfr).catch(() => {}) } } else { this.save_message = res.data.message || this.messages.saveError; diff --git a/dashboard/src/views/Settings.vue b/dashboard/src/views/Settings.vue index f7c401e4c..72bb2a4fb 100644 --- a/dashboard/src/views/Settings.vue +++ b/dashboard/src/views/Settings.vue @@ -78,12 +78,12 @@