diff --git a/dashboard/src/i18n/locales/en-US/core/header.json b/dashboard/src/i18n/locales/en-US/core/header.json index 080c59b132..4da98b8dd8 100644 --- a/dashboard/src/i18n/locales/en-US/core/header.json +++ b/dashboard/src/i18n/locales/en-US/core/header.json @@ -58,6 +58,18 @@ "guideStep2": "Install it and restart AstrBot.", "guideStep3": "If you use Docker, prefer the image update path." }, + "desktopApp": { + "title": "Update Desktop App", + "message": "Check and upgrade the AstrBot desktop application.", + "currentVersion": "Current version: ", + "latestVersion": "Latest version: ", + "checking": "Checking desktop app updates...", + "hasNewVersion": "A new version is available. Click confirm to upgrade.", + "isLatest": "Already on the latest version", + "installing": "Downloading and installing update. The app will restart automatically...", + "checkFailed": "Failed to check updates. Please try again later.", + "installFailed": "Upgrade failed. Please try again later." + }, "dashboardUpdate": { "title": "Update Dashboard to Latest Version Only", "currentVersion": "Current Version", diff --git a/dashboard/src/i18n/locales/zh-CN/core/header.json b/dashboard/src/i18n/locales/zh-CN/core/header.json index 8b6b3dc1e5..3bb9850331 100644 --- a/dashboard/src/i18n/locales/zh-CN/core/header.json +++ b/dashboard/src/i18n/locales/zh-CN/core/header.json @@ -58,6 +58,18 @@ "guideStep2": "完成安装后重启 AstrBot。", "guideStep3": "如果你使用 Docker,请优先使用镜像更新方式。" }, + "desktopApp": { + "title": "更新桌面应用", + "message": "将检查并升级 AstrBot 桌面端程序。", + "currentVersion": "当前版本:", + "latestVersion": "最新版本:", + "checking": "正在检查桌面应用更新...", + "hasNewVersion": "发现新版本,可点击确认升级。", + "isLatest": "已经是最新版本", + "installing": "正在下载并安装更新,完成后将自动重启应用...", + "checkFailed": "检查更新失败,请稍后重试。", + "installFailed": "升级失败,请稍后重试。" + }, "dashboardUpdate": { "title": "单独更新管理面板到最新版本", "currentVersion": "当前版本", diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index 543aef12cd..47905d4ff8 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -50,24 +50,27 @@ 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(''); + +const getAppUpdaterBridge = (): AstrBotAppUpdaterBridge | 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 +92,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 +119,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.error(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; } - 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.error(error); + 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 +714,38 @@ onMounted(async () => { - + - {{ t('core.header.updateDialog.redirectConfirm.title') }} + {{ t('core.header.updateDialog.desktopApp.title') }}
- {{ t('core.header.updateDialog.redirectConfirm.message') }} + {{ t('core.header.updateDialog.desktopApp.message') }}
- {{ t('core.header.updateDialog.redirectConfirm.targetVersion') }} - {{ latestReleaseTag }} - + {{ t('core.header.updateDialog.desktopApp.currentVersion') }} + {{ desktopUpdateCurrentVersion }}
-
- {{ t('core.header.updateDialog.redirectConfirm.currentVersion') }} - {{ botCurrVersion || '-' }} +
+ {{ t('core.header.updateDialog.desktopApp.latestVersion') }} + {{ desktopUpdateLatestVersion }} +
-
{{ t('core.header.updateDialog.redirectConfirm.guideTitle') }}
-
1. {{ t('core.header.updateDialog.redirectConfirm.guideStep1') }}
-
2. {{ t('core.header.updateDialog.redirectConfirm.guideStep2') }}
-
3. {{ t('core.header.updateDialog.redirectConfirm.guideStep3') }}
+ {{ desktopUpdateStatus }}
- + {{ t('core.common.dialog.cancelButton') }} - + {{ t('core.common.dialog.confirmButton') }} diff --git a/dashboard/src/types/electron-bridge.d.ts b/dashboard/src/types/desktop-bridge.d.ts similarity index 54% rename from dashboard/src/types/electron-bridge.d.ts rename to dashboard/src/types/desktop-bridge.d.ts index b42ffc0cf1..a51500b150 100644 --- a/dashboard/src/types/electron-bridge.d.ts +++ b/dashboard/src/types/desktop-bridge.d.ts @@ -1,7 +1,26 @@ export {}; declare global { + interface AstrBotDesktopAppUpdateCheckResult { + ok: boolean; + reason?: string | null; + currentVersion?: string; + latestVersion?: string | null; + hasUpdate: boolean; + } + + interface AstrBotDesktopAppUpdateResult { + ok: boolean; + reason?: string | null; + } + + interface AstrBotAppUpdaterBridge { + checkForAppUpdate: () => Promise; + installAppUpdate: () => Promise; + } + interface Window { + astrbotAppUpdater?: AstrBotAppUpdaterBridge; astrbotDesktop?: { isDesktop: boolean; isDesktopRuntime: () => Promise;