-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(dashboard): add generic desktop app updater bridge #5424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
6e5cdbf
55bbd09
1f74252
3f56e6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,24 +50,29 @@ let installLoading = ref(false); | |
| const isDesktopReleaseMode = ref( | ||
| typeof window !== 'undefined' && !!window.astrbotDesktop?.isDesktop | ||
| ); | ||
| const redirectConfirmDialog = ref(false); | ||
| const pendingRedirectUrl = ref(''); | ||
| const resolvingReleaseTarget = ref(false); | ||
| const DEFAULT_ASTRBOT_RELEASE_BASE_URL = 'https://github.com/AstrBotDevs/AstrBot/releases'; | ||
| const resolveReleaseBaseUrl = () => { | ||
| const raw = import.meta.env.VITE_ASTRBOT_RELEASE_BASE_URL; | ||
| // Keep upstream default on AstrBot releases; desktop distributors can override via env injection. | ||
| const normalized = raw?.trim()?.replace(/\/+$/, '') || ''; | ||
| const withoutLatestSuffix = normalized.replace(/\/latest$/i, ''); | ||
| return withoutLatestSuffix || DEFAULT_ASTRBOT_RELEASE_BASE_URL; | ||
| }; | ||
| const releaseBaseUrl = resolveReleaseBaseUrl(); | ||
| const getReleaseUrlByTag = (tag: string | null | undefined) => { | ||
| const normalizedTag = (tag || '').trim(); | ||
| if (!normalizedTag || normalizedTag.toLowerCase() === 'latest') { | ||
| return `${releaseBaseUrl}/latest`; | ||
| const desktopUpdateDialog = ref(false); | ||
| const desktopUpdateChecking = ref(false); | ||
| const desktopUpdateInstalling = ref(false); | ||
| const desktopUpdateHasNewVersion = ref(false); | ||
| const desktopUpdateCurrentVersion = ref('-'); | ||
| const desktopUpdateLatestVersion = ref('-'); | ||
| const desktopUpdateStatus = ref(''); | ||
|
|
||
| type AppUpdaterBridge = NonNullable<Window['astrbotAppUpdater']>; | ||
|
|
||
| const getAppUpdaterBridge = (): AppUpdaterBridge | null => { | ||
| if (typeof window === 'undefined') { | ||
| return null; | ||
| } | ||
| const bridge = window.astrbotAppUpdater; | ||
| if ( | ||
| bridge && | ||
| typeof bridge.checkForAppUpdate === 'function' && | ||
| typeof bridge.installAppUpdate === 'function' | ||
| ) { | ||
| return bridge; | ||
| } | ||
| return `${releaseBaseUrl}/tag/${normalizedTag}`; | ||
| return null; | ||
| }; | ||
|
|
||
| const getSelectedGitHubProxy = () => { | ||
|
|
@@ -89,16 +94,6 @@ const releasesHeader = computed(() => [ | |
| { title: t('core.header.updateDialog.table.sourceUrl'), key: 'zipball_url' }, | ||
| { title: t('core.header.updateDialog.table.actions'), key: 'switch' } | ||
| ]); | ||
| const latestReleaseTag = computed(() => { | ||
| const firstRelease = (releases.value as any[])?.[0]; | ||
| if (firstRelease?.tag_name) { | ||
| return firstRelease.tag_name as string; | ||
| } | ||
| return hasNewVersion.value | ||
| ? t('core.header.updateDialog.redirectConfirm.latestLabel') | ||
| : (botCurrVersion.value || '-'); | ||
| }); | ||
|
|
||
| // Form validation | ||
| const formValid = ref(true); | ||
| const passwordRules = computed(() => [ | ||
|
|
@@ -126,47 +121,88 @@ const accountEditStatus = ref({ | |
| message: '' | ||
| }); | ||
|
|
||
| const open = (link: string) => { | ||
| window.open(link, '_blank'); | ||
| }; | ||
| function cancelDesktopUpdate() { | ||
| if (desktopUpdateInstalling.value) { | ||
| return; | ||
| } | ||
| desktopUpdateDialog.value = false; | ||
| } | ||
|
|
||
| async function openDesktopUpdateDialog() { | ||
| desktopUpdateDialog.value = true; | ||
| desktopUpdateChecking.value = true; | ||
| desktopUpdateInstalling.value = false; | ||
| desktopUpdateHasNewVersion.value = false; | ||
| desktopUpdateCurrentVersion.value = '-'; | ||
| desktopUpdateLatestVersion.value = '-'; | ||
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checking'); | ||
|
|
||
| const bridge = getAppUpdaterBridge(); | ||
| if (!bridge) { | ||
| desktopUpdateChecking.value = false; | ||
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed'); | ||
| return; | ||
| } | ||
|
|
||
| function requestExternalRedirect(link: string) { | ||
| pendingRedirectUrl.value = link; | ||
| redirectConfirmDialog.value = true; | ||
| } | ||
| try { | ||
| const result = await bridge.checkForAppUpdate(); | ||
| if (!result?.ok) { | ||
| desktopUpdateCurrentVersion.value = result?.currentVersion || '-'; | ||
| desktopUpdateLatestVersion.value = | ||
| result?.latestVersion || result?.currentVersion || '-'; | ||
| desktopUpdateStatus.value = | ||
| result?.reason || t('core.header.updateDialog.desktopApp.checkFailed'); | ||
| return; | ||
| } | ||
|
|
||
| function cancelExternalRedirect() { | ||
| redirectConfirmDialog.value = false; | ||
| pendingRedirectUrl.value = ''; | ||
| desktopUpdateCurrentVersion.value = result.currentVersion || '-'; | ||
| desktopUpdateLatestVersion.value = | ||
| result.latestVersion || result.currentVersion || '-'; | ||
| desktopUpdateHasNewVersion.value = !!result.hasUpdate; | ||
| desktopUpdateStatus.value = result.hasUpdate | ||
| ? t('core.header.updateDialog.desktopApp.hasNewVersion') | ||
| : t('core.header.updateDialog.desktopApp.isLatest'); | ||
| } catch (error) { | ||
| console.log(error); | ||
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed'); | ||
| } finally { | ||
| desktopUpdateChecking.value = false; | ||
| } | ||
| } | ||
|
|
||
| function confirmExternalRedirect() { | ||
| const targetUrl = pendingRedirectUrl.value; | ||
| cancelExternalRedirect(); | ||
| if (targetUrl) { | ||
| open(targetUrl); | ||
| async function confirmDesktopUpdate() { | ||
| if (!desktopUpdateHasNewVersion.value || desktopUpdateInstalling.value) { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| const getReleaseUrlForDesktop = () => { | ||
| const firstRelease = (releases.value as any[])?.[0]; | ||
| if (firstRelease?.tag_name) { | ||
| return getReleaseUrlByTag(firstRelease.tag_name as string); | ||
| const bridge = getAppUpdaterBridge(); | ||
| if (!bridge) { | ||
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed'); | ||
| return; | ||
| } | ||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||
| if (hasNewVersion.value) return getReleaseUrlByTag('latest'); | ||
| const tag = botCurrVersion.value?.startsWith('v') ? botCurrVersion.value : 'latest'; | ||
| return getReleaseUrlByTag(tag); | ||
| }; | ||
|
|
||
| desktopUpdateInstalling.value = true; | ||
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installing'); | ||
|
|
||
| try { | ||
| const result = await bridge.installAppUpdate(); | ||
| if (result?.ok) { | ||
| desktopUpdateDialog.value = false; | ||
| return; | ||
| } | ||
| desktopUpdateStatus.value = | ||
| result?.reason || t('core.header.updateDialog.desktopApp.installFailed'); | ||
| } catch (error) { | ||
| console.log(error); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed'); | ||
| } finally { | ||
| desktopUpdateInstalling.value = false; | ||
| } | ||
| } | ||
|
|
||
| function handleUpdateClick() { | ||
| if (isDesktopReleaseMode.value) { | ||
| requestExternalRedirect(''); | ||
| resolvingReleaseTarget.value = true; | ||
| checkUpdate(); | ||
| void getReleases().finally(() => { | ||
| pendingRedirectUrl.value = getReleaseUrlForDesktop() || getReleaseUrlByTag('latest'); | ||
| resolvingReleaseTarget.value = false; | ||
| }); | ||
| void openDesktopUpdateDialog(); | ||
| return; | ||
| } | ||
| checkUpdate(); | ||
|
|
@@ -680,40 +716,38 @@ onMounted(async () => { | |
| </v-card> | ||
| </v-dialog> | ||
|
|
||
| <v-dialog v-model="redirectConfirmDialog" max-width="460"> | ||
| <v-dialog v-model="desktopUpdateDialog" max-width="460"> | ||
| <v-card> | ||
| <v-card-title class="text-h3 pa-4 pl-6 pb-0"> | ||
| {{ t('core.header.updateDialog.redirectConfirm.title') }} | ||
| {{ t('core.header.updateDialog.desktopApp.title') }} | ||
| </v-card-title> | ||
| <v-card-text> | ||
| <div class="mb-3"> | ||
| {{ t('core.header.updateDialog.redirectConfirm.message') }} | ||
| {{ t('core.header.updateDialog.desktopApp.message') }} | ||
| </div> | ||
| <v-alert type="info" variant="tonal" density="compact"> | ||
| <div> | ||
| {{ t('core.header.updateDialog.redirectConfirm.targetVersion') }} | ||
| <strong v-if="!resolvingReleaseTarget">{{ latestReleaseTag }}</strong> | ||
| <v-progress-circular v-else indeterminate size="16" width="2" class="ml-1" /> | ||
| {{ t('core.header.updateDialog.desktopApp.currentVersion') }} | ||
| <strong>{{ desktopUpdateCurrentVersion }}</strong> | ||
| </div> | ||
| <div class="text-caption"> | ||
| {{ t('core.header.updateDialog.redirectConfirm.currentVersion') }} | ||
| {{ botCurrVersion || '-' }} | ||
| <div> | ||
| {{ t('core.header.updateDialog.desktopApp.latestVersion') }} | ||
| <strong v-if="!desktopUpdateChecking">{{ desktopUpdateLatestVersion }}</strong> | ||
| <v-progress-circular v-else indeterminate size="16" width="2" class="ml-1" /> | ||
| </div> | ||
| </v-alert> | ||
| <div class="text-caption mt-3"> | ||
| <div>{{ t('core.header.updateDialog.redirectConfirm.guideTitle') }}</div> | ||
| <div>1. {{ t('core.header.updateDialog.redirectConfirm.guideStep1') }}</div> | ||
| <div>2. {{ t('core.header.updateDialog.redirectConfirm.guideStep2') }}</div> | ||
| <div>3. {{ t('core.header.updateDialog.redirectConfirm.guideStep3') }}</div> | ||
| {{ desktopUpdateStatus }} | ||
| </div> | ||
| </v-card-text> | ||
| <v-card-actions> | ||
| <v-spacer></v-spacer> | ||
| <v-btn color="grey" variant="text" @click="cancelExternalRedirect"> | ||
| <v-btn color="grey" variant="text" @click="cancelDesktopUpdate" :disabled="desktopUpdateInstalling"> | ||
| {{ t('core.common.dialog.cancelButton') }} | ||
| </v-btn> | ||
| <v-btn color="primary" variant="flat" @click="confirmExternalRedirect" | ||
| :loading="resolvingReleaseTarget" :disabled="resolvingReleaseTarget || !pendingRedirectUrl"> | ||
| <v-btn color="primary" variant="flat" @click="confirmDesktopUpdate" | ||
| :loading="desktopUpdateInstalling" | ||
| :disabled="desktopUpdateChecking || desktopUpdateInstalling || !desktopUpdateHasNewVersion"> | ||
| {{ t('core.common.dialog.confirmButton') }} | ||
| </v-btn> | ||
| </v-card-actions> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| export {}; | ||
|
|
||
| declare global { | ||
| interface AstrBotAppUpdaterBridge { | ||
| checkForAppUpdate: () => Promise<{ | ||
| ok: boolean; | ||
| reason: string | null; | ||
| currentVersion: string; | ||
| latestVersion: string | null; | ||
| hasUpdate: boolean; | ||
| }>; | ||
| installAppUpdate: () => Promise<{ | ||
| ok: boolean; | ||
| reason: string | null; | ||
| }>; | ||
| } | ||
|
|
||
| interface Window { | ||
| astrbotAppUpdater?: AstrBotAppUpdaterBridge; | ||
| astrbotDesktop?: { | ||
| isDesktop: boolean; | ||
| isDesktopRuntime: () => Promise<boolean>; | ||
| getBackendState: () => Promise<{ | ||
| running: boolean; | ||
| spawning: boolean; | ||
| restarting: boolean; | ||
| canManage: boolean; | ||
| }>; | ||
| restartBackend: (authToken?: string | null) => Promise<{ | ||
| ok: boolean; | ||
| reason: string | null; | ||
| }>; | ||
| stopBackend: () => Promise<{ | ||
| ok: boolean; | ||
| reason: string | null; | ||
| }>; | ||
| checkDesktopAppUpdate: () => Promise<{ | ||
| ok: boolean; | ||
| reason: string | null; | ||
| currentVersion: string; | ||
| latestVersion: string | null; | ||
| hasUpdate: boolean; | ||
| }>; | ||
| installDesktopAppUpdate: () => Promise<{ | ||
| ok: boolean; | ||
| reason: string | null; | ||
| }>; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| onTrayRestartBackend?: (callback: () => void) => () => void; | ||
| }; | ||
| } | ||
| } | ||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
console.log(error)statement is quite generic. For better debugging and monitoring in a production environment, consider adding more context to the log message, such as the function name (openDesktopUpdateDialog) and a descriptive string. This would help in quickly identifying the source of the error.