fix: handle legacy login upgrade recovery

This commit is contained in:
Soulter
2026-06-17 23:43:04 +08:00
parent 96474d3d84
commit 08fc565175
5 changed files with 114 additions and 21 deletions

View File

@@ -66,6 +66,9 @@ export interface ApiEnvelope<T> {
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<string, ProviderCapability> = {
rerank: 'rerank',
};
type V1Response<T> = Promise<AxiosResponse<ApiEnvelope<T>>>;
type V1Response<T> = Promise<
AxiosResponse<ApiEnvelope<T>> & { legacyFallback?: boolean }
>;
type ListConversationsQuery = NonNullable<ListConversationsData['query']>;
function typed<T>(response: Promise<unknown>): V1Response<T> {
@@ -199,9 +204,27 @@ function withLegacyFallback<T>(
primary: Promise<unknown>,
legacy: () => Promise<AxiosResponse<ApiEnvelope<T>>>,
): V1Response<T> {
return typed<T>(primary).catch((error) => {
const legacyRequest = () =>
legacy().then((response) => {
const legacyResponse = response as AxiosResponse<ApiEnvelope<T>> & {
legacyFallback?: boolean;
};
legacyResponse.legacyFallback = true;
return legacyResponse;
});
return typed<T>(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;
});

View File

@@ -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<string, string> = {};
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<VersionData>).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();
});
</script>

View File

@@ -22,7 +22,7 @@ interface AuthStore {
password: string,
code?: string,
trustDeviceToken?: boolean,
): Promise<void | 'totp_required'>;
): Promise<void | 'totp_required' | 'upgrade_recovery_required'>;
logout(): void;
has_token(): boolean;
}

View File

@@ -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<ApiEnvelope<VersionData>>(
'/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) {

View File

@@ -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';