Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 12 additions & 0 deletions dashboard/src/i18n/locales/en-US/core/header.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions dashboard/src/i18n/locales/zh-CN/core/header.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@
"guideStep2": "完成安装后重启 AstrBot。",
"guideStep3": "如果你使用 Docker,请优先使用镜像更新方式。"
},
"desktopApp": {
"title": "更新桌面应用",
"message": "将检查并升级 AstrBot 桌面端程序。",
"currentVersion": "当前版本:",
"latestVersion": "最新版本:",
"checking": "正在检查桌面应用更新...",
"hasNewVersion": "发现新版本,可点击确认升级。",
"isLatest": "已经是最新版本",
"installing": "正在下载并安装更新,完成后将自动重启应用...",
"checkFailed": "检查更新失败,请稍后重试。",
"installFailed": "升级失败,请稍后重试。"
},
"dashboardUpdate": {
"title": "单独更新管理面板到最新版本",
"currentVersion": "当前版本",
Expand Down
182 changes: 108 additions & 74 deletions dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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(() => [
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

    console.error('[DesktopUpdate] Failed to check for app update:', 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;
}
Comment thread
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the previous comment, this console.log(error) could benefit from more context. Providing the function name (confirmDesktopUpdate) and a descriptive message would improve debuggability.

    console.error('[DesktopUpdate] Failed to install app update:', 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();
Expand Down Expand Up @@ -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>
Expand Down
51 changes: 51 additions & 0 deletions dashboard/src/types/desktop-bridge.d.ts
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;
}>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The astrbotDesktop interface still includes checkDesktopAppUpdate and installDesktopAppUpdate. The pull request description mentions decoupling the dashboard from astrbotDesktop-specific methods by introducing a generic window.astrbotAppUpdater interface. To fully achieve this decoupling and avoid potential confusion or redundant implementations, these update-related methods should ideally be removed from astrbotDesktop and exclusively exposed through astrbotAppUpdater.

onTrayRestartBackend?: (callback: () => void) => () => void;
};
}
}
25 changes: 0 additions & 25 deletions dashboard/src/types/electron-bridge.d.ts

This file was deleted.