fix: preserve desktop session across restarts

This commit is contained in:
2026-06-18 23:51:30 +08:00
parent d8195bc935
commit 1523956839
4 changed files with 78 additions and 23 deletions
+2 -9
View File
@@ -1171,8 +1171,7 @@ if (!hasSingleInstanceLock) {
.then(async () => { .then(async () => {
suppressNativeWindowMenu() suppressNativeWindowMenu()
registerDesktopProtocolClient() registerDesktopProtocolClient()
currentWindowMode = 'login' currentWindowMode = readPersistedWindowMode()
persistWindowMode('login')
initDesktopAppSettings() initDesktopAppSettings()
registerBridgeHandlers() registerBridgeHandlers()
await initSessionRegistry() await initSessionRegistry()
@@ -1229,14 +1228,8 @@ if (!hasSingleInstanceLock) {
quitReleaseInFlight = true quitReleaseInFlight = true
event.preventDefault() event.preventDefault()
currentWindowMode = 'login'
persistWindowMode('login')
void Promise.race([ void Promise.race([
(async () => { releaseRuntimeSession(),
await releaseRuntimeSession()
await clearRendererDesktopSessions()
})(),
new Promise<void>((resolve) => setTimeout(resolve, 1500)), new Promise<void>((resolve) => setTimeout(resolve, 1500)),
]).finally(() => { ]).finally(() => {
app.quit() app.quit()
+11
View File
@@ -67,6 +67,17 @@ if (rendererWindowMode === 'main') {
}) })
} }
if (rendererWindowMode === 'login') {
if (isAuthenticated.value) {
syncWindowMode(true)
}
watch(isAuthenticated, (next) => {
if (next) {
syncWindowMode(true)
}
})
}
onMounted(() => { onMounted(() => {
if (rendererWindowMode !== 'login') { if (rendererWindowMode !== 'login') {
void syncRuntimeSession().catch((err) => { void syncRuntimeSession().catch((err) => {
@@ -68,11 +68,6 @@ function readStoredSession(): DesktopSession | null {
} }
try { try {
if (shouldDiscardStoredSessionForWindow()) {
window.localStorage.removeItem(sessionStorageKey)
return null
}
const raw = window.localStorage.getItem(sessionStorageKey) const raw = window.localStorage.getItem(sessionStorageKey)
if (!raw) { if (!raw) {
return null 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 { function normalizeStoredSession(value: unknown): DesktopSession | null {
if (!value || typeof value !== 'object') { if (!value || typeof value !== 'object') {
return null return null
@@ -166,6 +157,7 @@ function createAuthenticatedClient() {
return createApiClient({ return createApiClient({
baseURL: apiBaseURL.value, baseURL: apiBaseURL.value,
timeoutMs: 15000, timeoutMs: 15000,
clearTokensOnUnauthorized: false,
auth: { auth: {
getAccessToken: () => session.value?.accessToken ?? null, getAccessToken: () => session.value?.accessToken ?? null,
getRefreshToken: () => session.value?.refreshToken ?? null, getRefreshToken: () => session.value?.refreshToken ?? null,
@@ -181,7 +173,18 @@ function createAuthenticatedClient() {
refreshToken: tokens.refreshToken, 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<AuthTokens> { async refresh(refreshToken: string): Promise<AuthTokens> {
const next = await createPublicClient().post<RefreshResponse, { refresh_token: string }>( const next = await createPublicClient().post<RefreshResponse, { refresh_token: string }>(
'/api/auth/refresh', '/api/auth/refresh',
@@ -239,6 +242,14 @@ function expiresAtMs(value: number | null | undefined): number | null {
return value < 10_000_000_000 ? value * 1000 : value 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 { function isRenewDue(value: number | null | undefined, renewWindowMs: number): boolean {
const expiresAt = expiresAtMs(value) const expiresAt = expiresAtMs(value)
if (!expiresAt) { if (!expiresAt) {
@@ -247,6 +258,17 @@ function isRenewDue(value: number | null | undefined, renewWindowMs: number): bo
return expiresAt - Date.now() <= renewWindowMs 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<DesktopSession> { async function refreshLoginTokens(current: DesktopSession): Promise<DesktopSession> {
if (!current.refreshToken) { if (!current.refreshToken) {
throw new Error('missing_refresh_token') throw new Error('missing_refresh_token')
@@ -302,6 +324,16 @@ async function forceLogoutAfterRenewFailure(err: unknown): Promise<void> {
await transitionWindowForLogout() 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<void> { async function renewAuthenticatedSession(options: { force?: boolean } = {}): Promise<void> {
const current = session.value const current = session.value
if (current?.mode !== 'authenticated') { if (current?.mode !== 'authenticated') {
@@ -325,7 +357,15 @@ async function renewAuthenticatedSession(options: { force?: boolean } = {}): Pro
} }
if (needsAccessRefresh) { 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) { if (session.value !== expectedSession) {
return return
} }
@@ -334,7 +374,15 @@ async function renewAuthenticatedSession(options: { force?: boolean } = {}): Pro
} }
if (needsClientRotate) { 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) { if (session.value !== expectedSession) {
return return
} }
@@ -780,7 +828,7 @@ export function useDesktopSession() {
apiBaseURL: readonly(apiBaseURL), apiBaseURL: readonly(apiBaseURL),
pending: readonly(pending), pending: readonly(pending),
error: readonly(error), error: readonly(error),
isAuthenticated: computed(() => Boolean(session.value?.clientToken)), isAuthenticated: computed(() => hasUsableStoredSession(session.value)),
isPreviewMode: computed(() => session.value?.mode === 'preview'), isPreviewMode: computed(() => session.value?.mode === 'preview'),
setApiBaseURL: persistApiBaseURL, setApiBaseURL: persistApiBaseURL,
syncRuntimeSession: syncRuntimeSessionToMain, syncRuntimeSession: syncRuntimeSessionToMain,
+4 -1
View File
@@ -19,6 +19,7 @@ export interface CreateApiClientOptions {
baseURL?: string baseURL?: string
timeoutMs?: number timeoutMs?: number
auth?: AuthBindings auth?: AuthBindings
clearTokensOnUnauthorized?: boolean
} }
export class ApiClientError extends Error { 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)) throw markHandled(normalizeError(error))
} }