Skip to content

Commit 5530a22

Browse files
authored
feat(dashboard): add generic desktop app updater bridge (#5424)
* feat(dashboard): add generic desktop app updater bridge * fix(dashboard): address updater bridge review feedback * fix(dashboard): unify updater bridge types and error logging * fix(dashboard): consolidate updater bridge typings
1 parent c24de24 commit 5530a22

File tree

4 files changed

+149
-74
lines changed

4 files changed

+149
-74
lines changed

dashboard/src/i18n/locales/en-US/core/header.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@
5858
"guideStep2": "Install it and restart AstrBot.",
5959
"guideStep3": "If you use Docker, prefer the image update path."
6060
},
61+
"desktopApp": {
62+
"title": "Update Desktop App",
63+
"message": "Check and upgrade the AstrBot desktop application.",
64+
"currentVersion": "Current version: ",
65+
"latestVersion": "Latest version: ",
66+
"checking": "Checking desktop app updates...",
67+
"hasNewVersion": "A new version is available. Click confirm to upgrade.",
68+
"isLatest": "Already on the latest version",
69+
"installing": "Downloading and installing update. The app will restart automatically...",
70+
"checkFailed": "Failed to check updates. Please try again later.",
71+
"installFailed": "Upgrade failed. Please try again later."
72+
},
6173
"dashboardUpdate": {
6274
"title": "Update Dashboard to Latest Version Only",
6375
"currentVersion": "Current Version",

dashboard/src/i18n/locales/zh-CN/core/header.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@
5858
"guideStep2": "完成安装后重启 AstrBot。",
5959
"guideStep3": "如果你使用 Docker,请优先使用镜像更新方式。"
6060
},
61+
"desktopApp": {
62+
"title": "更新桌面应用",
63+
"message": "将检查并升级 AstrBot 桌面端程序。",
64+
"currentVersion": "当前版本:",
65+
"latestVersion": "最新版本:",
66+
"checking": "正在检查桌面应用更新...",
67+
"hasNewVersion": "发现新版本,可点击确认升级。",
68+
"isLatest": "已经是最新版本",
69+
"installing": "正在下载并安装更新,完成后将自动重启应用...",
70+
"checkFailed": "检查更新失败,请稍后重试。",
71+
"installFailed": "升级失败,请稍后重试。"
72+
},
6173
"dashboardUpdate": {
6274
"title": "单独更新管理面板到最新版本",
6375
"currentVersion": "当前版本",

dashboard/src/layouts/full/vertical-header/VerticalHeader.vue

Lines changed: 106 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,27 @@ let installLoading = ref(false);
5050
const isDesktopReleaseMode = ref(
5151
typeof window !== 'undefined' && !!window.astrbotDesktop?.isDesktop
5252
);
53-
const redirectConfirmDialog = ref(false);
54-
const pendingRedirectUrl = ref('');
55-
const resolvingReleaseTarget = ref(false);
56-
const DEFAULT_ASTRBOT_RELEASE_BASE_URL = 'https://github.com/AstrBotDevs/AstrBot/releases';
57-
const resolveReleaseBaseUrl = () => {
58-
const raw = import.meta.env.VITE_ASTRBOT_RELEASE_BASE_URL;
59-
// Keep upstream default on AstrBot releases; desktop distributors can override via env injection.
60-
const normalized = raw?.trim()?.replace(/\/+$/, '') || '';
61-
const withoutLatestSuffix = normalized.replace(/\/latest$/i, '');
62-
return withoutLatestSuffix || DEFAULT_ASTRBOT_RELEASE_BASE_URL;
63-
};
64-
const releaseBaseUrl = resolveReleaseBaseUrl();
65-
const getReleaseUrlByTag = (tag: string | null | undefined) => {
66-
const normalizedTag = (tag || '').trim();
67-
if (!normalizedTag || normalizedTag.toLowerCase() === 'latest') {
68-
return `${releaseBaseUrl}/latest`;
53+
const desktopUpdateDialog = ref(false);
54+
const desktopUpdateChecking = ref(false);
55+
const desktopUpdateInstalling = ref(false);
56+
const desktopUpdateHasNewVersion = ref(false);
57+
const desktopUpdateCurrentVersion = ref('-');
58+
const desktopUpdateLatestVersion = ref('-');
59+
const desktopUpdateStatus = ref('');
60+
61+
const getAppUpdaterBridge = (): AstrBotAppUpdaterBridge | null => {
62+
if (typeof window === 'undefined') {
63+
return null;
64+
}
65+
const bridge = window.astrbotAppUpdater;
66+
if (
67+
bridge &&
68+
typeof bridge.checkForAppUpdate === 'function' &&
69+
typeof bridge.installAppUpdate === 'function'
70+
) {
71+
return bridge;
6972
}
70-
return `${releaseBaseUrl}/tag/${normalizedTag}`;
73+
return null;
7174
};
7275
7376
const getSelectedGitHubProxy = () => {
@@ -89,16 +92,6 @@ const releasesHeader = computed(() => [
8992
{ title: t('core.header.updateDialog.table.sourceUrl'), key: 'zipball_url' },
9093
{ title: t('core.header.updateDialog.table.actions'), key: 'switch' }
9194
]);
92-
const latestReleaseTag = computed(() => {
93-
const firstRelease = (releases.value as any[])?.[0];
94-
if (firstRelease?.tag_name) {
95-
return firstRelease.tag_name as string;
96-
}
97-
return hasNewVersion.value
98-
? t('core.header.updateDialog.redirectConfirm.latestLabel')
99-
: (botCurrVersion.value || '-');
100-
});
101-
10295
// Form validation
10396
const formValid = ref(true);
10497
const passwordRules = computed(() => [
@@ -126,47 +119,88 @@ const accountEditStatus = ref({
126119
message: ''
127120
});
128121
129-
const open = (link: string) => {
130-
window.open(link, '_blank');
131-
};
122+
function cancelDesktopUpdate() {
123+
if (desktopUpdateInstalling.value) {
124+
return;
125+
}
126+
desktopUpdateDialog.value = false;
127+
}
128+
129+
async function openDesktopUpdateDialog() {
130+
desktopUpdateDialog.value = true;
131+
desktopUpdateChecking.value = true;
132+
desktopUpdateInstalling.value = false;
133+
desktopUpdateHasNewVersion.value = false;
134+
desktopUpdateCurrentVersion.value = '-';
135+
desktopUpdateLatestVersion.value = '-';
136+
desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checking');
137+
138+
const bridge = getAppUpdaterBridge();
139+
if (!bridge) {
140+
desktopUpdateChecking.value = false;
141+
desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed');
142+
return;
143+
}
132144
133-
function requestExternalRedirect(link: string) {
134-
pendingRedirectUrl.value = link;
135-
redirectConfirmDialog.value = true;
136-
}
145+
try {
146+
const result = await bridge.checkForAppUpdate();
147+
if (!result?.ok) {
148+
desktopUpdateCurrentVersion.value = result?.currentVersion || '-';
149+
desktopUpdateLatestVersion.value =
150+
result?.latestVersion || result?.currentVersion || '-';
151+
desktopUpdateStatus.value =
152+
result?.reason || t('core.header.updateDialog.desktopApp.checkFailed');
153+
return;
154+
}
137155
138-
function cancelExternalRedirect() {
139-
redirectConfirmDialog.value = false;
140-
pendingRedirectUrl.value = '';
156+
desktopUpdateCurrentVersion.value = result.currentVersion || '-';
157+
desktopUpdateLatestVersion.value =
158+
result.latestVersion || result.currentVersion || '-';
159+
desktopUpdateHasNewVersion.value = !!result.hasUpdate;
160+
desktopUpdateStatus.value = result.hasUpdate
161+
? t('core.header.updateDialog.desktopApp.hasNewVersion')
162+
: t('core.header.updateDialog.desktopApp.isLatest');
163+
} catch (error) {
164+
console.error(error);
165+
desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed');
166+
} finally {
167+
desktopUpdateChecking.value = false;
168+
}
141169
}
142170
143-
function confirmExternalRedirect() {
144-
const targetUrl = pendingRedirectUrl.value;
145-
cancelExternalRedirect();
146-
if (targetUrl) {
147-
open(targetUrl);
171+
async function confirmDesktopUpdate() {
172+
if (!desktopUpdateHasNewVersion.value || desktopUpdateInstalling.value) {
173+
return;
148174
}
149-
}
150175
151-
const getReleaseUrlForDesktop = () => {
152-
const firstRelease = (releases.value as any[])?.[0];
153-
if (firstRelease?.tag_name) {
154-
return getReleaseUrlByTag(firstRelease.tag_name as string);
176+
const bridge = getAppUpdaterBridge();
177+
if (!bridge) {
178+
desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed');
179+
return;
155180
}
156-
if (hasNewVersion.value) return getReleaseUrlByTag('latest');
157-
const tag = botCurrVersion.value?.startsWith('v') ? botCurrVersion.value : 'latest';
158-
return getReleaseUrlByTag(tag);
159-
};
181+
182+
desktopUpdateInstalling.value = true;
183+
desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installing');
184+
185+
try {
186+
const result = await bridge.installAppUpdate();
187+
if (result?.ok) {
188+
desktopUpdateDialog.value = false;
189+
return;
190+
}
191+
desktopUpdateStatus.value =
192+
result?.reason || t('core.header.updateDialog.desktopApp.installFailed');
193+
} catch (error) {
194+
console.error(error);
195+
desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed');
196+
} finally {
197+
desktopUpdateInstalling.value = false;
198+
}
199+
}
160200
161201
function handleUpdateClick() {
162202
if (isDesktopReleaseMode.value) {
163-
requestExternalRedirect('');
164-
resolvingReleaseTarget.value = true;
165-
checkUpdate();
166-
void getReleases().finally(() => {
167-
pendingRedirectUrl.value = getReleaseUrlForDesktop() || getReleaseUrlByTag('latest');
168-
resolvingReleaseTarget.value = false;
169-
});
203+
void openDesktopUpdateDialog();
170204
return;
171205
}
172206
checkUpdate();
@@ -680,40 +714,38 @@ onMounted(async () => {
680714
</v-card>
681715
</v-dialog>
682716

683-
<v-dialog v-model="redirectConfirmDialog" max-width="460">
717+
<v-dialog v-model="desktopUpdateDialog" max-width="460">
684718
<v-card>
685719
<v-card-title class="text-h3 pa-4 pl-6 pb-0">
686-
{{ t('core.header.updateDialog.redirectConfirm.title') }}
720+
{{ t('core.header.updateDialog.desktopApp.title') }}
687721
</v-card-title>
688722
<v-card-text>
689723
<div class="mb-3">
690-
{{ t('core.header.updateDialog.redirectConfirm.message') }}
724+
{{ t('core.header.updateDialog.desktopApp.message') }}
691725
</div>
692726
<v-alert type="info" variant="tonal" density="compact">
693727
<div>
694-
{{ t('core.header.updateDialog.redirectConfirm.targetVersion') }}
695-
<strong v-if="!resolvingReleaseTarget">{{ latestReleaseTag }}</strong>
696-
<v-progress-circular v-else indeterminate size="16" width="2" class="ml-1" />
728+
{{ t('core.header.updateDialog.desktopApp.currentVersion') }}
729+
<strong>{{ desktopUpdateCurrentVersion }}</strong>
697730
</div>
698-
<div class="text-caption">
699-
{{ t('core.header.updateDialog.redirectConfirm.currentVersion') }}
700-
{{ botCurrVersion || '-' }}
731+
<div>
732+
{{ t('core.header.updateDialog.desktopApp.latestVersion') }}
733+
<strong v-if="!desktopUpdateChecking">{{ desktopUpdateLatestVersion }}</strong>
734+
<v-progress-circular v-else indeterminate size="16" width="2" class="ml-1" />
701735
</div>
702736
</v-alert>
703737
<div class="text-caption mt-3">
704-
<div>{{ t('core.header.updateDialog.redirectConfirm.guideTitle') }}</div>
705-
<div>1. {{ t('core.header.updateDialog.redirectConfirm.guideStep1') }}</div>
706-
<div>2. {{ t('core.header.updateDialog.redirectConfirm.guideStep2') }}</div>
707-
<div>3. {{ t('core.header.updateDialog.redirectConfirm.guideStep3') }}</div>
738+
{{ desktopUpdateStatus }}
708739
</div>
709740
</v-card-text>
710741
<v-card-actions>
711742
<v-spacer></v-spacer>
712-
<v-btn color="grey" variant="text" @click="cancelExternalRedirect">
743+
<v-btn color="grey" variant="text" @click="cancelDesktopUpdate" :disabled="desktopUpdateInstalling">
713744
{{ t('core.common.dialog.cancelButton') }}
714745
</v-btn>
715-
<v-btn color="primary" variant="flat" @click="confirmExternalRedirect"
716-
:loading="resolvingReleaseTarget" :disabled="resolvingReleaseTarget || !pendingRedirectUrl">
746+
<v-btn color="primary" variant="flat" @click="confirmDesktopUpdate"
747+
:loading="desktopUpdateInstalling"
748+
:disabled="desktopUpdateChecking || desktopUpdateInstalling || !desktopUpdateHasNewVersion">
717749
{{ t('core.common.dialog.confirmButton') }}
718750
</v-btn>
719751
</v-card-actions>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
export {};
22

33
declare global {
4+
interface AstrBotDesktopAppUpdateCheckResult {
5+
ok: boolean;
6+
reason?: string | null;
7+
currentVersion?: string;
8+
latestVersion?: string | null;
9+
hasUpdate: boolean;
10+
}
11+
12+
interface AstrBotDesktopAppUpdateResult {
13+
ok: boolean;
14+
reason?: string | null;
15+
}
16+
17+
interface AstrBotAppUpdaterBridge {
18+
checkForAppUpdate: () => Promise<AstrBotDesktopAppUpdateCheckResult>;
19+
installAppUpdate: () => Promise<AstrBotDesktopAppUpdateResult>;
20+
}
21+
422
interface Window {
23+
astrbotAppUpdater?: AstrBotAppUpdaterBridge;
524
astrbotDesktop?: {
625
isDesktop: boolean;
726
isDesktopRuntime: () => Promise<boolean>;

0 commit comments

Comments
 (0)