From 08fc5651750ef91413a2b38a8a0c9d2cfb1f2f1d Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 17 Jun 2026 23:43:04 +0800 Subject: [PATCH] fix: handle legacy login upgrade recovery --- dashboard/src/api/v1.ts | 29 +++++++++-- .../shared/UpgradeRecoveryDialog.vue | 52 +++++++++++++------ dashboard/src/router/index.ts | 2 +- dashboard/src/stores/auth.ts | 50 +++++++++++++++++- .../authentication/authForms/AuthLogin.vue | 2 + 5 files changed, 114 insertions(+), 21 deletions(-) diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index 0cdb2d838..b0b248de5 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -66,6 +66,9 @@ export interface ApiEnvelope { data: T; } +export const UPGRADE_RECOVERY_EVENT = 'astrbot-upgrade-recovery'; +export const UPGRADE_RECOVERY_TOKEN_KEY = 'astrbot-upgrade-recovery-token'; + export type OpenConfig = DynamicConfig; export interface ProviderSchemaData { @@ -173,7 +176,9 @@ const PROVIDER_TYPE_TO_CAPABILITY: Record = { rerank: 'rerank', }; -type V1Response = Promise>>; +type V1Response = Promise< + AxiosResponse> & { legacyFallback?: boolean } +>; type ListConversationsQuery = NonNullable; function typed(response: Promise): V1Response { @@ -199,9 +204,27 @@ function withLegacyFallback( primary: Promise, legacy: () => Promise>>, ): V1Response { - return typed(primary).catch((error) => { + const legacyRequest = () => + legacy().then((response) => { + const legacyResponse = response as AxiosResponse> & { + legacyFallback?: boolean; + }; + legacyResponse.legacyFallback = true; + return legacyResponse; + }); + + return typed(primary).then((response) => { + const message = response.data?.message || ''; + if ( + response.data?.status === 'error' && + message.toLowerCase().includes('missing api key') + ) { + return legacyRequest(); + } + return response; + }).catch((error) => { if (isLegacyFallbackError(error)) { - return legacy(); + return legacyRequest(); } throw error; }); diff --git a/dashboard/src/components/shared/UpgradeRecoveryDialog.vue b/dashboard/src/components/shared/UpgradeRecoveryDialog.vue index fdb8ecf31..896fb7dca 100644 --- a/dashboard/src/components/shared/UpgradeRecoveryDialog.vue +++ b/dashboard/src/components/shared/UpgradeRecoveryDialog.vue @@ -52,7 +52,12 @@ import axios, { type AxiosRequestConfig } from 'axios'; import { onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { useRoute } from 'vue-router'; -import type { ApiEnvelope, VersionData } from '@/api/v1'; +import { + UPGRADE_RECOVERY_EVENT, + UPGRADE_RECOVERY_TOKEN_KEY, + type ApiEnvelope, + type VersionData, +} from '@/api/v1'; import { useI18n } from '@/i18n/composables'; type StartTimeData = { @@ -108,7 +113,9 @@ function getDismissKey() { function recoveryRequestConfig(validateStatus = false): AxiosRequestConfig { const headers: Record = {}; - const token = localStorage.getItem('token'); + const token = + localStorage.getItem('token') || + sessionStorage.getItem(UPGRADE_RECOVERY_TOKEN_KEY); const locale = localStorage.getItem('astrbot-locale'); if (token) { headers.Authorization = `Bearer ${token}`; @@ -139,6 +146,7 @@ function clearRestartTimer() { function dismiss() { sessionStorage.setItem(getDismissKey(), '1'); + sessionStorage.removeItem(UPGRADE_RECOVERY_TOKEN_KEY); visible.value = false; } @@ -154,6 +162,7 @@ function waitForRestart() { String(nextStartTime) !== String(initialStartTime.value) ) { clearRestartTimer(); + sessionStorage.removeItem(UPGRADE_RECOVERY_TOKEN_KEY); window.location.reload(); } } catch (_error) { @@ -187,6 +196,29 @@ async function restartCore() { } } +async function showRecoveryDialog(versionData: VersionData) { + if (visible.value || restarting.value) { + return; + } + if (!versionsMismatch(versionData.version, versionData.dashboard_version)) { + return; + } + + coreVersion.value = displayVersion(versionData.version); + dashboardVersion.value = displayVersion(versionData.dashboard_version); + if (sessionStorage.getItem(getDismissKey())) { + return; + } + + initialStartTime.value = await fetchLegacyStartTime().catch(() => null); + visible.value = true; +} + +function handleRecoveryEvent(event: Event) { + const versionData = (event as CustomEvent).detail || {}; + void showRecoveryDialog(versionData); +} + async function detectUpgradeMismatch() { if (detecting || visible.value || restarting.value) { return; @@ -209,19 +241,7 @@ async function detectUpgradeMismatch() { return; } - const versionData = legacyResponse.data?.data || {}; - if (!versionsMismatch(versionData.version, versionData.dashboard_version)) { - return; - } - - coreVersion.value = displayVersion(versionData.version); - dashboardVersion.value = displayVersion(versionData.dashboard_version); - if (sessionStorage.getItem(getDismissKey())) { - return; - } - - initialStartTime.value = await fetchLegacyStartTime().catch(() => null); - visible.value = true; + await showRecoveryDialog(legacyResponse.data?.data || {}); } catch (_error) { // This recovery dialog is best-effort and should never block the app. } finally { @@ -230,6 +250,7 @@ async function detectUpgradeMismatch() { } onMounted(() => { + window.addEventListener(UPGRADE_RECOVERY_EVENT, handleRecoveryEvent); void detectUpgradeMismatch(); }); @@ -241,6 +262,7 @@ watch( ); onBeforeUnmount(() => { + window.removeEventListener(UPGRADE_RECOVERY_EVENT, handleRecoveryEvent); clearRestartTimer(); }); diff --git a/dashboard/src/router/index.ts b/dashboard/src/router/index.ts index 85a99f65f..2eab46697 100644 --- a/dashboard/src/router/index.ts +++ b/dashboard/src/router/index.ts @@ -22,7 +22,7 @@ interface AuthStore { password: string, code?: string, trustDeviceToken?: boolean, - ): Promise; + ): Promise; logout(): void; has_token(): boolean; } diff --git a/dashboard/src/stores/auth.ts b/dashboard/src/stores/auth.ts index 948607816..b666fe60d 100644 --- a/dashboard/src/stores/auth.ts +++ b/dashboard/src/stores/auth.ts @@ -1,6 +1,15 @@ import { defineStore } from 'pinia'; import { router } from '@/router'; -import { authApi, providerApi, systemConfigApi } from '@/api/v1'; +import { + authApi, + providerApi, + systemConfigApi, + UPGRADE_RECOVERY_EVENT, + UPGRADE_RECOVERY_TOKEN_KEY, + type ApiEnvelope, + type VersionData, +} from '@/api/v1'; +import { httpClient } from '@/api/http'; export const useAuthStore = defineStore("auth", { state: () => ({ @@ -52,7 +61,7 @@ export const useAuthStore = defineStore("auth", { password: string, code?: string, trustDeviceToken = false, - ): Promise<'totp_required' | void> { + ): Promise<'totp_required' | 'upgrade_recovery_required' | void> { try { const res = await authApi.login({ username, @@ -65,6 +74,43 @@ export const useAuthStore = defineStore("auth", { return Promise.reject(res.data.message); } + const legacyToken = String(res.data.data?.token || ''); + if (res.legacyFallback && legacyToken) { + const versionRes = await httpClient.get>( + '/api/stat/version', + { + headers: { + Authorization: `Bearer ${legacyToken}`, + }, + validateStatus: () => true, + }, + ); + const versionData = versionRes.data?.data || {}; + const coreVersion = String(versionData.version || '') + .trim() + .replace(/^v/i, ''); + const dashboardVersion = String(versionData.dashboard_version || '') + .trim() + .replace(/^v/i, ''); + if ( + versionRes.status < 400 && + coreVersion && + dashboardVersion && + coreVersion !== dashboardVersion + ) { + sessionStorage.setItem(UPGRADE_RECOVERY_TOKEN_KEY, legacyToken); + window.dispatchEvent( + new CustomEvent(UPGRADE_RECOVERY_EVENT, { + detail: { + version: versionData.version, + dashboard_version: versionData.dashboard_version, + }, + }), + ); + return 'upgrade_recovery_required'; + } + } + await this.finishAuthenticatedSession(res.data.data); } catch (error: any) { if (error?.response?.status === 401 && error.response?.data?.data?.totp_required) { diff --git a/dashboard/src/views/authentication/authForms/AuthLogin.vue b/dashboard/src/views/authentication/authForms/AuthLogin.vue index 4a8967dd0..7d1618b5a 100644 --- a/dashboard/src/views/authentication/authForms/AuthLogin.vue +++ b/dashboard/src/views/authentication/authForms/AuthLogin.vue @@ -52,6 +52,8 @@ async function submitAccountStage() { const res = await authStore.login(username.value, password.value); if (res === 'totp_required') { goToTotpStage(); + } else if (res === 'upgrade_recovery_required') { + return; } } catch (err) { apiError.value = String(err || '') || 'Login failed';