Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 72 additions & 19 deletions astrbot/core/updator.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,35 +44,88 @@ def terminate_child_processes(self) -> None:
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]

@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:
"""重启当前程序
在指定的延迟后,终止所有子进程并重新启动程序
这里只能使用 os.exec* 来重启程序
"""
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)
self._exec_reboot(executable, reboot_argv)
except Exception as e:
logger.error(f"重启失败({py}, {e}),请尝试手动重启。")
logger.error(f"重启失败({executable}, {e}),请尝试手动重启。")
raise e

async def check_update(
Expand Down
13 changes: 7 additions & 6 deletions dashboard/src/components/shared/BackupDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
}

// 重置所有状态
Expand Down
13 changes: 7 additions & 6 deletions dashboard/src/components/shared/MigrationDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)
}
}

// 打开对话框的方法
Expand Down
21 changes: 15 additions & 6 deletions dashboard/src/components/shared/WaitingForRestart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,31 @@ 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(() => {
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) {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/types/electron-bridge.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ declare global {
restarting: boolean;
canManage: boolean;
}>;
restartBackend: () => Promise<{
restartBackend: (authToken?: string | null) => Promise<{
ok: boolean;
reason: string | null;
}>;
Expand Down
52 changes: 52 additions & 0 deletions dashboard/src/utils/restartAstrBot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import axios from 'axios'

type WaitingForRestartRef = {
check: (initialStartTime?: number | null) => void | Promise<void>
stop?: () => void
}

async function triggerWaiting(
waitingRef?: WaitingForRestartRef | null,
initialStartTime?: number | null
) {
if (!waitingRef) return
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(
waitingRef?: WaitingForRestartRef | null
): Promise<void> {
const desktopBridge = window.astrbotDesktop

if (desktopBridge?.isElectron) {
const authToken = localStorage.getItem('token')
const initialStartTime = await fetchCurrentStartTime()
try {
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.')
}
} catch (error) {
waitingRef?.stop?.()
throw error
}
return
}

await axios.post('/api/stat/restart-core')
await triggerWaiting(waitingRef)
}
5 changes: 2 additions & 3 deletions dashboard/src/views/ConfigPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 7 additions & 5 deletions dashboard/src/views/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@

<script setup>
import { ref, watch } from 'vue';
import axios from 'axios';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import ProxySelector from '@/components/shared/ProxySelector.vue';
import MigrationDialog from '@/components/shared/MigrationDialog.vue';
import SidebarCustomizer from '@/components/shared/SidebarCustomizer.vue';
import BackupDialog from '@/components/shared/BackupDialog.vue';
import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot';
import { useModuleI18n } from '@/i18n/composables';
import { useTheme } from 'vuetify';
import { PurpleTheme } from '@/theme/LightTheme';
Expand Down Expand Up @@ -136,10 +136,12 @@ const wfr = ref(null);
const migrationDialog = ref(null);
const backupDialog = ref(null);

const restartAstrBot = () => {
axios.post('/api/stat/restart-core').then(() => {
wfr.value.check();
})
const restartAstrBot = async () => {
try {
await restartAstrBotRuntime(wfr.value);
} catch (error) {
console.error(error);
}
}

const startMigration = async () => {
Expand Down
Loading
Loading