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
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -58,6 +58,18 @@
|
||||
"guideStep2": "完成安装后重启 AstrBot。",
|
||||
"guideStep3": "如果你使用 Docker,请优先使用镜像更新方式。"
|
||||
},
|
||||
"desktopApp": {
|
||||
"title": "更新桌面应用",
|
||||
"message": "将检查并升级 AstrBot 桌面端程序。",
|
||||
"currentVersion": "当前版本:",
|
||||
"latestVersion": "最新版本:",
|
||||
"checking": "正在检查桌面应用更新...",
|
||||
"hasNewVersion": "发现新版本,可点击确认升级。",
|
||||
"isLatest": "已经是最新版本",
|
||||
"installing": "正在下载并安装更新,完成后将自动重启应用...",
|
||||
"checkFailed": "检查更新失败,请稍后重试。",
|
||||
"installFailed": "升级失败,请稍后重试。"
|
||||
},
|
||||
"dashboardUpdate": {
|
||||
"title": "单独更新管理面板到最新版本",
|
||||
"currentVersion": "当前版本",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
return `${releaseBaseUrl}/tag/${normalizedTag}`;
|
||||
const bridge = window.astrbotAppUpdater;
|
||||
if (
|
||||
bridge &&
|
||||
typeof bridge.checkForAppUpdate === 'function' &&
|
||||
typeof bridge.installAppUpdate === 'function'
|
||||
) {
|
||||
return bridge;
|
||||
}
|
||||
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 requestExternalRedirect(link: string) {
|
||||
pendingRedirectUrl.value = link;
|
||||
redirectConfirmDialog.value = true;
|
||||
function cancelDesktopUpdate() {
|
||||
if (desktopUpdateInstalling.value) {
|
||||
return;
|
||||
}
|
||||
desktopUpdateDialog.value = false;
|
||||
}
|
||||
|
||||
function cancelExternalRedirect() {
|
||||
redirectConfirmDialog.value = false;
|
||||
pendingRedirectUrl.value = '';
|
||||
}
|
||||
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');
|
||||
|
||||
function confirmExternalRedirect() {
|
||||
const targetUrl = pendingRedirectUrl.value;
|
||||
cancelExternalRedirect();
|
||||
if (targetUrl) {
|
||||
open(targetUrl);
|
||||
const bridge = getAppUpdaterBridge();
|
||||
if (!bridge) {
|
||||
desktopUpdateChecking.value = false;
|
||||
desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed');
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const getReleaseUrlForDesktop = () => {
|
||||
const firstRelease = (releases.value as any[])?.[0];
|
||||
if (firstRelease?.tag_name) {
|
||||
return getReleaseUrlByTag(firstRelease.tag_name as string);
|
||||
async function confirmDesktopUpdate() {
|
||||
if (!desktopUpdateHasNewVersion.value || desktopUpdateInstalling.value) {
|
||||
return;
|
||||
}
|
||||
if (hasNewVersion.value) return getReleaseUrlByTag('latest');
|
||||
const tag = botCurrVersion.value?.startsWith('v') ? botCurrVersion.value : 'latest';
|
||||
return getReleaseUrlByTag(tag);
|
||||
};
|
||||
|
||||
const bridge = getAppUpdaterBridge();
|
||||
if (!bridge) {
|
||||
desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed');
|
||||
return;
|
||||
}
|
||||
|
||||
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 () => {
|
||||
</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>
|
||||
|
||||
+19
@@ -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<AstrBotDesktopAppUpdateCheckResult>;
|
||||
installAppUpdate: () => Promise<AstrBotDesktopAppUpdateResult>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
astrbotAppUpdater?: AstrBotAppUpdaterBridge;
|
||||
astrbotDesktop?: {
|
||||
isDesktop: boolean;
|
||||
isDesktopRuntime: () => Promise<boolean>;
|
||||
Reference in New Issue
Block a user