mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix(startup): retry initialization after CLI Web login
Defer one-shot background initialization when the presentation request discovers authentication is required, so preferences, LLM state, and sessions initialize only after a successful login.
This commit is contained in:
1 parent
55c9e1dd25
commit
b8f1c37eb2
5 files changed
+115
-6
No files matched your search
+5
-2
@@ -158,6 +158,8 @@ async function initializeApp() {
|
||||
await settingsStore.initLocale()
|
||||
markStartupPhase('locale-settled')
|
||||
},
|
||||
deferAfterPresentationError: () =>
|
||||
PLATFORM_CAPABILITIES.requiresAuth && authStore.requiresAuth && !authStore.isAuthenticated,
|
||||
initializeBackground: [
|
||||
{
|
||||
name: 'preferences',
|
||||
@@ -196,6 +198,8 @@ async function initializeApp() {
|
||||
],
|
||||
listenForPullResults: () => apiServerStore.listenPullResult(),
|
||||
})
|
||||
// 401 会先切换到登录页;不要把这次未认证尝试标记为就绪,登录后由 route watcher 完整重试。
|
||||
if (result.deferred) return
|
||||
unlistenPullResult ??= result.stopListeningForPullResults
|
||||
presentationWarning.value = result.presentationError !== null
|
||||
markStartupPhase('runtime-ready')
|
||||
@@ -345,8 +349,7 @@ onMounted(async () => {
|
||||
const on401 = () => {
|
||||
if (redirectingTo401 || router.currentRoute.value.name === 'login') return
|
||||
redirectingTo401 = true
|
||||
authStore.markRequiresAuth()
|
||||
authStore.logout()
|
||||
authStore.requireLogin()
|
||||
const currentPath = router.currentRoute.value.fullPath
|
||||
const redirect = currentPath.startsWith('/login') ? '/' : currentPath
|
||||
router.push({ name: 'login', query: { redirect } }).finally(() => {
|
||||
|
||||
@@ -151,6 +151,7 @@ test('reveals the shell after presentation state and runs independent startup wo
|
||||
])
|
||||
assert.equal(result.presentationError, null)
|
||||
assert.equal(result.stopListeningForPullResults, stop)
|
||||
assert.equal(result.deferred, false)
|
||||
|
||||
releaseTasks.forEach((release) => release())
|
||||
assert.deepEqual(await result.background, [])
|
||||
@@ -176,5 +177,49 @@ test('uses the presentation fallback on timeout and reports background failures
|
||||
|
||||
assert.match(String(result.presentationError), /timed out after 5ms/)
|
||||
assert.equal(applyLatePresentation, false)
|
||||
assert.equal(result.deferred, false)
|
||||
assert.deepEqual(await result.background, [{ name: 'sessions', error: failure }])
|
||||
})
|
||||
|
||||
test('defers one-shot background work until authentication can recover presentation loading', async () => {
|
||||
const calls: string[] = []
|
||||
let authenticated = false
|
||||
|
||||
const initialize = () =>
|
||||
initializeProgressiveAppRuntime({
|
||||
initializeServices: async () => void calls.push('services'),
|
||||
loadPresentation: async () => {
|
||||
calls.push('presentation')
|
||||
if (!authenticated) throw new Error('HTTP 401')
|
||||
return { locale: 'zh-CN' }
|
||||
},
|
||||
applyPresentation: () => void calls.push('apply-presentation'),
|
||||
applyPresentationFallback: () => void calls.push('fallback'),
|
||||
deferAfterPresentationError: () => !authenticated,
|
||||
initializeBackground: [
|
||||
{ name: 'preferences', run: async () => void calls.push('preferences') },
|
||||
{ name: 'llm', run: async () => void calls.push('llm') },
|
||||
{ name: 'sessions', run: async () => void calls.push('sessions') },
|
||||
],
|
||||
})
|
||||
|
||||
const deferred = await initialize()
|
||||
assert.equal(deferred.deferred, true)
|
||||
assert.deepEqual(await deferred.background, [])
|
||||
assert.deepEqual(calls, ['services', 'presentation'])
|
||||
|
||||
authenticated = true
|
||||
const recovered = await initialize()
|
||||
assert.equal(recovered.deferred, false)
|
||||
assert.deepEqual(await recovered.background, [])
|
||||
assert.deepEqual(calls, [
|
||||
'services',
|
||||
'presentation',
|
||||
'services',
|
||||
'presentation',
|
||||
'apply-presentation',
|
||||
'preferences',
|
||||
'llm',
|
||||
'sessions',
|
||||
])
|
||||
})
|
||||
@@ -29,6 +29,7 @@ export interface ProgressiveInitializationPorts<TPresentation> {
|
||||
loadPresentation(): Promise<TPresentation>
|
||||
applyPresentation(presentation: TPresentation): Promise<void> | void
|
||||
applyPresentationFallback(): Promise<void> | void
|
||||
deferAfterPresentationError?: (error: unknown) => boolean
|
||||
initializeBackground: Array<{
|
||||
name: string
|
||||
run(): Promise<void>
|
||||
@@ -46,6 +47,7 @@ export interface ProgressiveInitializationResult {
|
||||
presentationError: unknown | null
|
||||
background: Promise<BackgroundInitializationFailure[]>
|
||||
stopListeningForPullResults: (() => void) | null
|
||||
deferred: boolean
|
||||
}
|
||||
|
||||
class PresentationTimeoutError extends Error {
|
||||
@@ -84,6 +86,16 @@ export async function initializeProgressiveAppRuntime<TPresentation>(
|
||||
await ports.applyPresentation(presentation)
|
||||
} catch (error) {
|
||||
presentationError = error
|
||||
// 认证等可恢复门禁失败时,不能启动会写入一次性状态的后台任务;
|
||||
// 调用方完成外部恢复后,应重新执行整条初始化链路。
|
||||
if (ports.deferAfterPresentationError?.(error)) {
|
||||
return {
|
||||
presentationError,
|
||||
background: Promise.resolve([]),
|
||||
stopListeningForPullResults: null,
|
||||
deferred: true,
|
||||
}
|
||||
}
|
||||
await ports.applyPresentationFallback()
|
||||
}
|
||||
|
||||
@@ -94,7 +106,7 @@ export async function initializeProgressiveAppRuntime<TPresentation>(
|
||||
)
|
||||
)
|
||||
|
||||
return { presentationError, background, stopListeningForPullResults }
|
||||
return { presentationError, background, stopListeningForPullResults, deferred: false }
|
||||
}
|
||||
|
||||
export async function initializeAppRuntime(ports: AppInitializationPorts): Promise<AppInitializationResult> {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useAuthStore } from './auth'
|
||||
|
||||
function createMemoryStorage(): Storage {
|
||||
const values = new Map<string, string>()
|
||||
return {
|
||||
get length() {
|
||||
return values.size
|
||||
},
|
||||
clear: () => values.clear(),
|
||||
getItem: (key) => values.get(key) ?? null,
|
||||
key: (index) => [...values.keys()][index] ?? null,
|
||||
removeItem: (key) => values.delete(key),
|
||||
setItem: (key, value) => void values.set(key, value),
|
||||
}
|
||||
}
|
||||
|
||||
test('keeps authentication required after rejected credentials and allows a later login', () => {
|
||||
Object.defineProperties(globalThis, {
|
||||
localStorage: { configurable: true, value: createMemoryStorage() },
|
||||
sessionStorage: { configurable: true, value: createMemoryStorage() },
|
||||
})
|
||||
setActivePinia(createPinia())
|
||||
|
||||
const auth = useAuthStore()
|
||||
auth.login('expired-token', false)
|
||||
auth.requireLogin()
|
||||
|
||||
assert.equal(auth.token, '')
|
||||
assert.equal(auth.isAuthenticated, false)
|
||||
assert.equal(auth.requiresAuth, true)
|
||||
assert.equal(sessionStorage.getItem('chatlab_auth_token'), null)
|
||||
|
||||
auth.login('valid-token', false)
|
||||
assert.equal(auth.isAuthenticated, true)
|
||||
assert.equal(auth.requiresAuth, true)
|
||||
})
|
||||
+13
-3
@@ -43,14 +43,24 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
function clearCredentials() {
|
||||
token.value = ''
|
||||
rememberDevice.value = false
|
||||
requiresAuth.value = false
|
||||
sessionStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(REMEMBER_KEY)
|
||||
}
|
||||
|
||||
return { token, requiresAuth, rememberDevice, isAuthenticated, login, logout, markRequiresAuth }
|
||||
function logout() {
|
||||
clearCredentials()
|
||||
requiresAuth.value = false
|
||||
}
|
||||
|
||||
/** Clear rejected credentials while preserving the server's authentication requirement. */
|
||||
function requireLogin() {
|
||||
clearCredentials()
|
||||
requiresAuth.value = true
|
||||
}
|
||||
|
||||
return { token, requiresAuth, rememberDevice, isAuthenticated, login, logout, requireLogin, markRequiresAuth }
|
||||
})
|
||||
Reference in new issue
Block a user