From 15239568399f80f2410a8def893838e591815564 Mon Sep 17 00:00:00 2001 From: liangxu Date: Thu, 18 Jun 2026 23:51:30 +0800 Subject: [PATCH] fix: preserve desktop session across restarts --- apps/desktop-client/src/main/bootstrap.ts | 11 +-- apps/desktop-client/src/renderer/App.vue | 11 +++ .../renderer/composables/useDesktopSession.ts | 74 +++++++++++++++---- packages/http-client/src/index.ts | 5 +- 4 files changed, 78 insertions(+), 23 deletions(-) diff --git a/apps/desktop-client/src/main/bootstrap.ts b/apps/desktop-client/src/main/bootstrap.ts index f54a6b2..fe3f60a 100644 --- a/apps/desktop-client/src/main/bootstrap.ts +++ b/apps/desktop-client/src/main/bootstrap.ts @@ -1171,8 +1171,7 @@ if (!hasSingleInstanceLock) { .then(async () => { suppressNativeWindowMenu() registerDesktopProtocolClient() - currentWindowMode = 'login' - persistWindowMode('login') + currentWindowMode = readPersistedWindowMode() initDesktopAppSettings() registerBridgeHandlers() await initSessionRegistry() @@ -1229,14 +1228,8 @@ if (!hasSingleInstanceLock) { quitReleaseInFlight = true event.preventDefault() - currentWindowMode = 'login' - persistWindowMode('login') - void Promise.race([ - (async () => { - await releaseRuntimeSession() - await clearRendererDesktopSessions() - })(), + releaseRuntimeSession(), new Promise((resolve) => setTimeout(resolve, 1500)), ]).finally(() => { app.quit() diff --git a/apps/desktop-client/src/renderer/App.vue b/apps/desktop-client/src/renderer/App.vue index 984f02b..9dd9061 100644 --- a/apps/desktop-client/src/renderer/App.vue +++ b/apps/desktop-client/src/renderer/App.vue @@ -67,6 +67,17 @@ if (rendererWindowMode === 'main') { }) } +if (rendererWindowMode === 'login') { + if (isAuthenticated.value) { + syncWindowMode(true) + } + watch(isAuthenticated, (next) => { + if (next) { + syncWindowMode(true) + } + }) +} + onMounted(() => { if (rendererWindowMode !== 'login') { void syncRuntimeSession().catch((err) => { diff --git a/apps/desktop-client/src/renderer/composables/useDesktopSession.ts b/apps/desktop-client/src/renderer/composables/useDesktopSession.ts index 00a3f76..e33515a 100644 --- a/apps/desktop-client/src/renderer/composables/useDesktopSession.ts +++ b/apps/desktop-client/src/renderer/composables/useDesktopSession.ts @@ -68,11 +68,6 @@ function readStoredSession(): DesktopSession | null { } try { - if (shouldDiscardStoredSessionForWindow()) { - window.localStorage.removeItem(sessionStorageKey) - return null - } - const raw = window.localStorage.getItem(sessionStorageKey) if (!raw) { return null @@ -83,10 +78,6 @@ function readStoredSession(): DesktopSession | null { } } -function shouldDiscardStoredSessionForWindow(): boolean { - return new URLSearchParams(window.location.search).get('window') === 'login' -} - function normalizeStoredSession(value: unknown): DesktopSession | null { if (!value || typeof value !== 'object') { return null @@ -166,6 +157,7 @@ function createAuthenticatedClient() { return createApiClient({ baseURL: apiBaseURL.value, timeoutMs: 15000, + clearTokensOnUnauthorized: false, auth: { getAccessToken: () => session.value?.accessToken ?? null, getRefreshToken: () => session.value?.refreshToken ?? null, @@ -181,7 +173,18 @@ function createAuthenticatedClient() { refreshToken: tokens.refreshToken, }) }, - clearTokens: () => persistSession(null), + clearTokens: () => { + if (!session.value) { + return + } + + persistSession({ + ...session.value, + accessToken: null, + accessTokenExpiresAt: null, + refreshToken: null, + }) + }, async refresh(refreshToken: string): Promise { const next = await createPublicClient().post( '/api/auth/refresh', @@ -239,6 +242,14 @@ function expiresAtMs(value: number | null | undefined): number | null { return value < 10_000_000_000 ? value * 1000 : value } +function hasUsableStoredSession(value: DesktopSession | null): boolean { + return Boolean( + value?.clientToken && + value.desktopClient && + (value.mode === 'authenticated' || value.mode === 'preview'), + ) +} + function isRenewDue(value: number | null | undefined, renewWindowMs: number): boolean { const expiresAt = expiresAtMs(value) if (!expiresAt) { @@ -247,6 +258,17 @@ function isRenewDue(value: number | null | undefined, renewWindowMs: number): bo return expiresAt - Date.now() <= renewWindowMs } +function isApiAuthExpiredError(err: unknown): boolean { + return err instanceof ApiClientError && err.status === 401 +} + +function isTransientRenewalError(err: unknown): boolean { + if (err instanceof ApiClientError) { + return !err.status || err.status >= 500 || err.message === 'request_timeout' + } + return true +} + async function refreshLoginTokens(current: DesktopSession): Promise { if (!current.refreshToken) { throw new Error('missing_refresh_token') @@ -302,6 +324,16 @@ async function forceLogoutAfterRenewFailure(err: unknown): Promise { await transitionWindowForLogout() } +function keepSessionAfterRenewFailure(err: unknown): void { + console.warn('[desktop-session] token renewal deferred', err) + error.value = isTransientRenewalError(err) + ? '登录状态暂时无法刷新,将保持当前客户端登录状态并自动重试' + : '客户端登录状态刷新失败,将保持当前会话并自动重试' + void syncRuntimeSessionToMain().catch((syncError) => { + logRuntimeSessionSyncFailure('renewal-deferred', syncError) + }) +} + async function renewAuthenticatedSession(options: { force?: boolean } = {}): Promise { const current = session.value if (current?.mode !== 'authenticated') { @@ -325,7 +357,15 @@ async function renewAuthenticatedSession(options: { force?: boolean } = {}): Pro } if (needsAccessRefresh) { - next = await refreshLoginTokens(next) + try { + next = await refreshLoginTokens(next) + } catch (err) { + if (isApiAuthExpiredError(err)) { + throw err + } + keepSessionAfterRenewFailure(err) + return + } if (session.value !== expectedSession) { return } @@ -334,7 +374,15 @@ async function renewAuthenticatedSession(options: { force?: boolean } = {}): Pro } if (needsClientRotate) { - next = await rotateRuntimeClientToken(next) + try { + next = await rotateRuntimeClientToken(next) + } catch (err) { + if (isApiAuthExpiredError(err)) { + throw err + } + keepSessionAfterRenewFailure(err) + return + } if (session.value !== expectedSession) { return } @@ -780,7 +828,7 @@ export function useDesktopSession() { apiBaseURL: readonly(apiBaseURL), pending: readonly(pending), error: readonly(error), - isAuthenticated: computed(() => Boolean(session.value?.clientToken)), + isAuthenticated: computed(() => hasUsableStoredSession(session.value)), isPreviewMode: computed(() => session.value?.mode === 'preview'), setApiBaseURL: persistApiBaseURL, syncRuntimeSession: syncRuntimeSessionToMain, diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index dbe660a..8550789 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -19,6 +19,7 @@ export interface CreateApiClientOptions { baseURL?: string timeoutMs?: number auth?: AuthBindings + clearTokensOnUnauthorized?: boolean } export class ApiClientError extends Error { @@ -154,7 +155,9 @@ export function createApiClient(options: CreateApiClientOptions = {}): ApiClient } } - auth.clearTokens() + if (options.clearTokensOnUnauthorized !== false) { + auth.clearTokens() + } throw markHandled(normalizeError(error)) }