mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 17:47:06 +08:00
fix: handle legacy login upgrade recovery
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user