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 () => {
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<void>((resolve) => setTimeout(resolve, 1500)),
]).finally(() => {
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(() => {
if (rendererWindowMode !== 'login') {
void syncRuntimeSession().catch((err) => {
@@ -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<AuthTokens> {
const next = await createPublicClient().post<RefreshResponse, { refresh_token: string }>(
'/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<DesktopSession> {
if (!current.refreshToken) {
throw new Error('missing_refresh_token')
@@ -302,6 +324,16 @@ async function forceLogoutAfterRenewFailure(err: unknown): Promise<void> {
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> {
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,