Coming back to / from /people could show the previous rectification session
as a locked page: its title in the header, the composer stuck on
"正在打开生时校正…", and the new-chat greeting in the middle.
The state that survived between pages is the sessionStorage login-return
stash that secondary-page sidebar links write from the current ?c=:
- /people「和 TA 对话」used a second intent (?newChat=1) parsed by a
component mounted inside Home after bootstrap, so the bootstrap new-chat
branch never ran and the stash won.
- A stash id not in the current person's loaded list was looked up and
landed with urlAction "keep", which assumes ?c= is already in the address
bar. It was not, so the rectification auto-open never fired. The stash
also ignored which person was current.
- An in-page new chat left the stash in place.
Fix: delete NewChatDeepLink / ?newChat and route「和 TA 对话」through
newChatHref(); a looked-up stash writes ?c= back (replace-selected) and is
dropped when it belongs to another person; startNewChat and
openChatBoundToProfile clear the stash. ?c= deep links, BUG-989 and BUG-705
are unchanged.
Tests: new real-lifecycle suite mounting the real Home, sidebar and people
page (10 cases: four secondary pages + mobile drawer, 和 TA 对话 for self and
another person, out-of-scope stash, same-person stash beyond the first page,
in-page new chat), plus two contract/unit tests. Six fail on origin/staging,
all pass here. Full suite 3928 / 61 failing, failure names identical to the
0ab061b9 baseline (3916 / 61).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
482 lines
19 KiB
TypeScript
482 lines
19 KiB
TypeScript
/**
|
|
* Home cloud bootstrap. No React hooks: Home owns one effect and passes setters.
|
|
*
|
|
* The 20s timeout aborts this run and reveals the fatal screen. A login redirect
|
|
* leaves that timer running so a failed navigation can still escape the spinner.
|
|
*/
|
|
|
|
import type { BirthTimeAssessmentPhase } from "@/components/birth-time-assessment-overlay";
|
|
import {
|
|
guidedBirthTimePreview,
|
|
isGuidedBirthTimePreview,
|
|
previewRectificationJourney,
|
|
} from "@/lib/birth-time-guided-preview";
|
|
import { hydrateDeclaredWindowDraft } from "@/lib/birth-time-intake-model";
|
|
import type { JourneyClientResponse } from "@/lib/birth-time-journey-client";
|
|
import {
|
|
SESSION_MISSING_NOTICE,
|
|
clearLoginSessionReturn,
|
|
writeSessionUrl,
|
|
type BootstrapSessionSelection,
|
|
} from "@/lib/chat-session-url";
|
|
import { createStartGreeting } from "@/lib/starter-greeting";
|
|
import {
|
|
HOME_BOOTSTRAP_SLOW_MS,
|
|
resolveStarterHomeLandingSessionId,
|
|
starterHomeLandingNeedsConsultation,
|
|
type BootstrapPhase,
|
|
} from "@/lib/home-bootstrap";
|
|
import {
|
|
ConsultationStatusError,
|
|
LoginRedirectError,
|
|
applyLegacySessionControls,
|
|
createSession,
|
|
fetchActiveConsultationStatus,
|
|
fetchConsultationStatus,
|
|
fetchModelCatalog,
|
|
fetchSessionDetail,
|
|
friendlyError,
|
|
patchSessionModel,
|
|
readSessions,
|
|
readStoredPendingConsultation,
|
|
redirectToLogin,
|
|
resolveLookupBootstrap,
|
|
} from "@/lib/home-cloud-sync";
|
|
import { chartSnapshotForSession, missingProfileStep, readProfile } from "@/lib/home-profile";
|
|
import { peekSubjectCatalog, readCurrentSubjectId } from "@/lib/current-subject";
|
|
import { sessionMatchesSubject } from "@/lib/chat-profile-picker-model";
|
|
import {
|
|
emptyProfile,
|
|
pendingConsultationStorageKey,
|
|
previewModelCatalog,
|
|
timestamp,
|
|
type Account,
|
|
type ChatSession,
|
|
type ConsultationStatus,
|
|
type OnboardingStep,
|
|
type Profile,
|
|
type SessionReadResult,
|
|
type StoredPendingConsultation,
|
|
} from "@/lib/home-types";
|
|
import type { PublicLanguageModelCatalog } from "@/lib/public-models";
|
|
import { dropRectificationStoredPending } from "@/lib/rectification-session-composer-guard";
|
|
import type { SessionListBoot } from "@/lib/session-list-context";
|
|
import { isUnsavedEmptyConsultation } from "@/lib/session-list-filter";
|
|
import { clearStaleClientReload } from "@/lib/stale-client-recovery";
|
|
|
|
export const HOME_BOOTSTRAP_TIMEOUT_MESSAGE = "连接云端服务超时。请检查网络后重试,或返回登录页重新建立会话。";
|
|
|
|
export type HomeBootstrapStorage = Pick<Storage, "getItem" | "removeItem">;
|
|
|
|
export type HomeBootstrapCommit = {
|
|
setAccount: (account: Account) => void;
|
|
setAccountError: (value: string) => void;
|
|
setActiveSessionId: (id: string) => void;
|
|
setBirthTimeAssessmentPhase: (phase: BirthTimeAssessmentPhase | null) => void;
|
|
setBirthTimeJourney: (journey: JourneyClientResponse | null) => void;
|
|
setBootstrapPhase: (phase: BootstrapPhase) => void;
|
|
setGuidedJourneyPreview: (value: boolean) => void;
|
|
setHydrated: (value: boolean) => void;
|
|
setModelCatalog: (catalog: PublicLanguageModelCatalog | null) => void;
|
|
setOnboardingStep: (step: OnboardingStep) => void;
|
|
setProfile: (profile: Profile) => void;
|
|
setProfileDraft: (profile: Profile) => void;
|
|
setSessions: (sessions: ChatSession[]) => void;
|
|
setSessionsCursor: (cursor: string | null) => void;
|
|
setStartGreeting: (value: string) => void;
|
|
};
|
|
|
|
export type HomeBootstrapIo = {
|
|
fetchModelCatalog: typeof fetchModelCatalog;
|
|
fetchActiveConsultationStatus: typeof fetchActiveConsultationStatus;
|
|
fetchConsultationStatus: typeof fetchConsultationStatus;
|
|
fetchSessionDetail: typeof fetchSessionDetail;
|
|
resolveLookupBootstrap: typeof resolveLookupBootstrap;
|
|
patchSessionModel: typeof patchSessionModel;
|
|
redirectToLogin: typeof redirectToLogin;
|
|
clearStaleClientReload: typeof clearStaleClientReload;
|
|
writeSessionUrl: typeof writeSessionUrl;
|
|
clearLoginSessionReturn: typeof clearLoginSessionReturn;
|
|
restoreConsultationRecovery: (
|
|
session: ChatSession,
|
|
requestId: string,
|
|
stored?: StoredPendingConsultation | null,
|
|
) => void;
|
|
};
|
|
|
|
export type HomeBootstrapDeps = {
|
|
sessionListReady: Promise<void>;
|
|
waitForSessionList?: () => Promise<void>;
|
|
sessionListBoot: () => SessionListBoot | null;
|
|
uiPreview: { current: boolean };
|
|
uiPreviewMode: { current: string | null };
|
|
consultationStatusMissingCount: { current: number };
|
|
isDevelopment: boolean;
|
|
readSearch: () => string;
|
|
storage: HomeBootstrapStorage;
|
|
restoreConsultationRecovery: HomeBootstrapIo["restoreConsultationRecovery"];
|
|
setComposerNotice: (notice: string) => void;
|
|
commit: HomeBootstrapCommit;
|
|
io?: Partial<HomeBootstrapIo>;
|
|
};
|
|
|
|
type AccountPhaseReady = {
|
|
kind: "ready";
|
|
nextAccount: Account;
|
|
nextModelCatalog: PublicLanguageModelCatalog | null;
|
|
nextProfile: Profile;
|
|
nextSessions: ChatSession[];
|
|
parsedSessions: SessionReadResult;
|
|
reservedConsultation: ConsultationStatus | null;
|
|
storedPending: StoredPendingConsultation | null;
|
|
cursor: string | null;
|
|
};
|
|
|
|
type AccountPhaseResult = AccountPhaseReady | { kind: "stop" };
|
|
|
|
const defaultIo: Omit<HomeBootstrapIo, "restoreConsultationRecovery"> = {
|
|
fetchModelCatalog,
|
|
fetchActiveConsultationStatus,
|
|
fetchConsultationStatus,
|
|
fetchSessionDetail,
|
|
resolveLookupBootstrap,
|
|
patchSessionModel,
|
|
redirectToLogin,
|
|
clearStaleClientReload,
|
|
writeSessionUrl,
|
|
clearLoginSessionReturn,
|
|
};
|
|
|
|
function resolveIo(deps: HomeBootstrapDeps): HomeBootstrapIo {
|
|
return {
|
|
...defaultIo,
|
|
...deps.io,
|
|
restoreConsultationRecovery: deps.io?.restoreConsultationRecovery ?? deps.restoreConsultationRecovery,
|
|
};
|
|
}
|
|
|
|
export async function runAccountPhase(
|
|
deps: HomeBootstrapDeps,
|
|
signal: AbortSignal,
|
|
): Promise<AccountPhaseResult> {
|
|
const io = resolveIo(deps);
|
|
const previewMode = deps.isDevelopment
|
|
? new URLSearchParams(deps.readSearch()).get("preview")
|
|
: null;
|
|
if (previewMode) {
|
|
deps.uiPreview.current = true;
|
|
deps.uiPreviewMode.current = previewMode;
|
|
deps.commit.setGuidedJourneyPreview(true);
|
|
if (previewMode === "error") {
|
|
deps.commit.setAccountError(HOME_BOOTSTRAP_TIMEOUT_MESSAGE);
|
|
deps.commit.setHydrated(true);
|
|
return { kind: "stop" };
|
|
}
|
|
const isAssessmentLoadingPreview = previewMode === "birth-time-assessment-loading";
|
|
const isCompletedCandidatePreview = previewMode === "birth-time-candidate-complete";
|
|
const isRectificationPreview = isGuidedBirthTimePreview(previewMode);
|
|
const previewJourney = isRectificationPreview
|
|
? guidedBirthTimePreview(previewMode)
|
|
: previewRectificationJourney;
|
|
const previewProfile: Profile = previewMode === "onboarding"
|
|
? emptyProfile
|
|
: {
|
|
...emptyProfile,
|
|
name: "林遥",
|
|
date: "1990-06-15",
|
|
time: isCompletedCandidatePreview ? "04:53" : isRectificationPreview || isAssessmentLoadingPreview ? "" : "12:30",
|
|
reportedTime: isRectificationPreview ? "14:30" : isAssessmentLoadingPreview || isCompletedCandidatePreview ? "" : "12:30",
|
|
birthTimeSource: isRectificationPreview ? "approximate" : isAssessmentLoadingPreview || isCompletedCandidatePreview ? "period_only" : "legacy_import",
|
|
...hydrateDeclaredWindowDraft({
|
|
period: isAssessmentLoadingPreview || isCompletedCandidatePreview ? "early_morning" : "",
|
|
}),
|
|
birthTimeClue: "",
|
|
uncertaintyBeforeMinutes: isRectificationPreview ? 30 : null,
|
|
uncertaintyAfterMinutes: isRectificationPreview ? 30 : null,
|
|
birthTimeStatus: isCompletedCandidatePreview
|
|
? "candidate"
|
|
: isAssessmentLoadingPreview
|
|
? "rectifying"
|
|
: previewJourney.snapshot.state === "candidate"
|
|
|| previewJourney.snapshot.state === "confirming"
|
|
|| previewJourney.snapshot.state === "ready"
|
|
? "candidate"
|
|
: isRectificationPreview ? "rectifying" : "confirmed",
|
|
rectificationCaseId: isRectificationPreview ? previewJourney.caseId : "",
|
|
countryCode: "CN",
|
|
provinceCode: "110000",
|
|
cityCode: "110000-city",
|
|
districtCode: "110101",
|
|
};
|
|
const previewMessages = previewMode === "conversation" || previewMode === "streaming" || previewMode === "partial"
|
|
? [
|
|
{ role: "user" as const, text: "未来半年是否适合换工作?" },
|
|
{ role: "assistant" as const, text: "可以先看职业方向、关键时间。\n同时评估现实风险。\n此处只展示本地预览,\n不调用真实星盘。" },
|
|
]
|
|
: [];
|
|
const previewSession: ChatSession = {
|
|
id: "preview-session",
|
|
title: previewMessages.length > 0 ? "未来半年是否适合换工作" : "新对话",
|
|
theme: "career",
|
|
modelId: previewModelCatalog.defaultModelId,
|
|
messages: previewMessages,
|
|
// session-clock-exempt: development preview fixture, not a stored session
|
|
updatedAt: timestamp(),
|
|
sessionType: "consultation",
|
|
rectificationCaseId: null,
|
|
chartProfileId: "self",
|
|
chartProfileName: previewProfile.name.trim() || "我",
|
|
chartProfileRole: "self",
|
|
pinned: false,
|
|
archivedAt: null,
|
|
messagesHydrated: true,
|
|
};
|
|
deps.commit.setAccount({
|
|
user: { id: "preview-user", email: "preview@local.test" },
|
|
avatar: null,
|
|
credits: 8,
|
|
isAdmin: false,
|
|
adminUrl: null,
|
|
rectificationPriceCredits: 1,
|
|
hasConfirmedBirthTime: previewProfile.birthTimeStatus === "confirmed",
|
|
hasUsableBirthTime: previewProfile.birthTimeStatus === "accepted" || previewProfile.birthTimeStatus === "confirmed",
|
|
activeSubscription: null,
|
|
profile: previewProfile,
|
|
});
|
|
deps.commit.setModelCatalog(previewModelCatalog);
|
|
deps.commit.setProfile(previewProfile);
|
|
deps.commit.setProfileDraft(previewProfile);
|
|
if (isRectificationPreview) {
|
|
deps.commit.setBirthTimeJourney(previewJourney);
|
|
}
|
|
if (isAssessmentLoadingPreview) {
|
|
deps.commit.setBirthTimeAssessmentPhase("assessing");
|
|
}
|
|
deps.commit.setOnboardingStep(isAssessmentLoadingPreview ? "birth" : missingProfileStep(previewProfile) ?? "name");
|
|
deps.commit.setSessions([previewSession]);
|
|
deps.commit.setActiveSessionId(previewSession.id);
|
|
const previewGreeting = previewProfile.name.trim() ? createStartGreeting(previewProfile.name) : "";
|
|
deps.commit.setStartGreeting(previewGreeting);
|
|
deps.commit.setHydrated(true);
|
|
return { kind: "stop" };
|
|
}
|
|
|
|
// Start independent reads before waiting for the provider so slow-network
|
|
// latency is spent in parallel, not as a serial account -> model chain.
|
|
const modelCatalogPromise = io.fetchModelCatalog(signal)
|
|
.then((catalog) => ({ catalog, error: null as unknown }))
|
|
.catch((error: unknown) => ({ catalog: null, error }));
|
|
const activeStatusPromise = io.fetchActiveConsultationStatus(signal).catch((caught: unknown) => {
|
|
if (caught instanceof Error && caught.name === "AbortError") throw caught;
|
|
return null;
|
|
});
|
|
await (deps.waitForSessionList?.() ?? deps.sessionListReady);
|
|
if (signal.aborted) return { kind: "stop" };
|
|
const listBoot = deps.sessionListBoot();
|
|
if (listBoot?.signedOut) {
|
|
io.redirectToLogin();
|
|
return { kind: "stop" };
|
|
}
|
|
if (!listBoot?.account || listBoot.error) throw new Error("session_list_unavailable");
|
|
const nextAccount = listBoot.account;
|
|
const modelCatalogResult = await modelCatalogPromise;
|
|
if (modelCatalogResult.error) throw modelCatalogResult.error;
|
|
const nextModelCatalog = modelCatalogResult.catalog;
|
|
const nextProfile = readProfile(nextAccount.profile);
|
|
const parsedSessions = readSessions(listBoot.rawRows, nextModelCatalog);
|
|
let nextSessions = parsedSessions.sessions.length > 0 ? parsedSessions.sessions : listBoot.sessions;
|
|
if (nextSessions.length === 0) {
|
|
nextSessions = [createSession(
|
|
nextModelCatalog?.defaultModelId ?? "",
|
|
"consultation",
|
|
chartSnapshotForSession(readCurrentSubjectId(), [...(peekSubjectCatalog() ?? [])], nextProfile),
|
|
)];
|
|
}
|
|
nextSessions = applyLegacySessionControls(nextAccount.user.id, nextSessions);
|
|
|
|
let reservedConsultation: ConsultationStatus | null = await activeStatusPromise;
|
|
const storedPending = dropRectificationStoredPending(
|
|
readStoredPendingConsultation(deps.storage.getItem(pendingConsultationStorageKey), nextSessions.map((session) => session.id)),
|
|
nextSessions,
|
|
);
|
|
if (!storedPending) deps.storage.removeItem(pendingConsultationStorageKey);
|
|
|
|
if (storedPending) {
|
|
try {
|
|
const status = await io.fetchConsultationStatus(
|
|
storedPending.sessionId,
|
|
storedPending.requestId,
|
|
signal,
|
|
);
|
|
if (status.status === "reserved") {
|
|
deps.consultationStatusMissingCount.current = 0;
|
|
reservedConsultation = status;
|
|
} else {
|
|
deps.storage.removeItem(pendingConsultationStorageKey);
|
|
reservedConsultation = null;
|
|
}
|
|
} catch (caught) {
|
|
if (caught instanceof Error && caught.name === "AbortError") throw caught;
|
|
deps.consultationStatusMissingCount.current = caught instanceof ConsultationStatusError && caught.status === 404 ? 1 : 0;
|
|
reservedConsultation = {
|
|
sessionId: storedPending.sessionId,
|
|
requestId: storedPending.requestId,
|
|
status: "reserved",
|
|
};
|
|
}
|
|
}
|
|
|
|
if (signal.aborted) return { kind: "stop" };
|
|
return {
|
|
kind: "ready",
|
|
nextAccount,
|
|
nextModelCatalog,
|
|
nextProfile,
|
|
nextSessions,
|
|
parsedSessions,
|
|
reservedConsultation,
|
|
storedPending,
|
|
cursor: listBoot.cursor,
|
|
};
|
|
}
|
|
|
|
export async function resolveLanding(
|
|
deps: HomeBootstrapDeps,
|
|
signal: AbortSignal,
|
|
ready: AccountPhaseReady,
|
|
): Promise<void> {
|
|
const io = resolveIo(deps);
|
|
const { writeSessionUrl, clearLoginSessionReturn, restoreConsultationRecovery } = io;
|
|
const lookedUp = await io.resolveLookupBootstrap({
|
|
sessions: ready.nextSessions,
|
|
catalog: ready.nextModelCatalog,
|
|
defaultSessionId: ready.nextSessions[0].id,
|
|
signal,
|
|
inSubjectScope: (session) => sessionMatchesSubject(session, readCurrentSubjectId()),
|
|
});
|
|
let nextSessions = lookedUp.sessions;
|
|
const bootstrapSelection: BootstrapSessionSelection = lookedUp.selection;
|
|
let landingSessionId = resolveStarterHomeLandingSessionId(
|
|
nextSessions,
|
|
bootstrapSelection.sessionId,
|
|
bootstrapSelection.urlAction,
|
|
);
|
|
if (starterHomeLandingNeedsConsultation(nextSessions, landingSessionId, bootstrapSelection.urlAction)) {
|
|
const homeSession = createSession(
|
|
ready.nextModelCatalog?.defaultModelId ?? "",
|
|
"consultation",
|
|
chartSnapshotForSession(readCurrentSubjectId(), [...(peekSubjectCatalog() ?? [])], ready.nextProfile),
|
|
);
|
|
nextSessions = [homeSession, ...nextSessions];
|
|
landingSessionId = homeSession.id;
|
|
}
|
|
const activeListed = nextSessions.find((session) => session.id === landingSessionId)
|
|
?? nextSessions[0];
|
|
if (activeListed && !activeListed.messagesHydrated && activeListed.sessionType === "consultation") {
|
|
try {
|
|
const detailed = await io.fetchSessionDetail(activeListed.id, ready.nextModelCatalog, signal);
|
|
if (detailed) {
|
|
nextSessions = nextSessions.map((session) => session.id === detailed.id ? detailed : session);
|
|
}
|
|
} catch (caught) {
|
|
if (caught instanceof Error && caught.name === "AbortError") throw caught;
|
|
if (caught instanceof LoginRedirectError) return;
|
|
}
|
|
}
|
|
if (signal.aborted) return;
|
|
io.clearStaleClientReload(deps.storage);
|
|
deps.commit.setSessionsCursor(ready.cursor);
|
|
deps.commit.setAccount(ready.nextAccount);
|
|
deps.commit.setModelCatalog(ready.nextModelCatalog);
|
|
deps.commit.setProfile(ready.nextProfile);
|
|
deps.commit.setProfileDraft(ready.nextProfile);
|
|
deps.commit.setStartGreeting(ready.nextProfile.name.trim() ? createStartGreeting(ready.nextProfile.name) : "");
|
|
deps.commit.setOnboardingStep(missingProfileStep(ready.nextProfile) ?? "name");
|
|
deps.commit.setSessions(nextSessions);
|
|
if (bootstrapSelection.urlAction === "new-chat") writeSessionUrl(null, "replace");
|
|
deps.commit.setActiveSessionId(landingSessionId);
|
|
if (bootstrapSelection.clearStoredReturn) clearLoginSessionReturn();
|
|
if (bootstrapSelection.urlAction === "replace-clear") writeSessionUrl(null, "replace");
|
|
if (bootstrapSelection.urlAction === "replace-selected") {
|
|
const selected = nextSessions.find((session) => session.id === bootstrapSelection.sessionId);
|
|
if (!selected || !isUnsavedEmptyConsultation(selected)) {
|
|
writeSessionUrl(bootstrapSelection.sessionId, "replace");
|
|
}
|
|
}
|
|
const reservedConsultation = ready.reservedConsultation;
|
|
const storedPending = ready.storedPending;
|
|
if (reservedConsultation?.status === "reserved") {
|
|
const recoverySession = nextSessions.find((session) => session.id === reservedConsultation.sessionId);
|
|
if (recoverySession) restoreConsultationRecovery(recoverySession, reservedConsultation.requestId, storedPending);
|
|
if (bootstrapSelection.urlAction === "new-chat") {
|
|
deps.commit.setActiveSessionId(landingSessionId);
|
|
deps.setComposerNotice("");
|
|
}
|
|
}
|
|
if (ready.parsedSessions.fallbackSessionIds.length > 0) {
|
|
deps.setComposerNotice("此前选择的模型已下线,已切换为默认模型。");
|
|
} else if (lookedUp.notice) {
|
|
deps.setComposerNotice(lookedUp.notice);
|
|
} else if (bootstrapSelection.missing) {
|
|
deps.setComposerNotice(SESSION_MISSING_NOTICE);
|
|
}
|
|
deps.commit.setAccountError("");
|
|
|
|
const fallbackCatalog = ready.nextModelCatalog;
|
|
if (fallbackCatalog && ready.parsedSessions.fallbackSessionIds.length > 0) {
|
|
try {
|
|
await Promise.all(ready.parsedSessions.fallbackSessionIds.map((sessionId) =>
|
|
io.patchSessionModel(sessionId, fallbackCatalog.defaultModelId, signal),
|
|
));
|
|
} catch {
|
|
if (!signal.aborted) {
|
|
deps.setComposerNotice("已在当前页面切换为默认模型,但云端同步失败;刷新后可能需要重新选择。");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function runHomeBootstrap(deps: HomeBootstrapDeps, signal: AbortSignal): Promise<void> {
|
|
const controller = new AbortController();
|
|
const onParentAbort = () => controller.abort();
|
|
if (signal.aborted) onParentAbort();
|
|
else signal.addEventListener("abort", onParentAbort);
|
|
|
|
const bootstrapTimeout = window.setTimeout(() => {
|
|
if (controller.signal.aborted) return;
|
|
controller.abort();
|
|
deps.commit.setAccountError(HOME_BOOTSTRAP_TIMEOUT_MESSAGE);
|
|
deps.commit.setHydrated(true);
|
|
}, HOME_BOOTSTRAP_SLOW_MS);
|
|
const clearTimeoutOnParentAbort = () => {
|
|
window.clearTimeout(bootstrapTimeout);
|
|
};
|
|
if (signal.aborted) clearTimeoutOnParentAbort();
|
|
else signal.addEventListener("abort", clearTimeoutOnParentAbort);
|
|
|
|
let redirectedToLogin = false;
|
|
let bootstrapFailed = false;
|
|
try {
|
|
const ready = await runAccountPhase(deps, controller.signal);
|
|
if (ready.kind === "ready") await resolveLanding(deps, controller.signal, ready);
|
|
} catch (caught) {
|
|
if (caught instanceof LoginRedirectError) {
|
|
redirectedToLogin = true;
|
|
return;
|
|
}
|
|
if ((caught as Error).name !== "AbortError" && !controller.signal.aborted) {
|
|
bootstrapFailed = true;
|
|
deps.commit.setAccountError(friendlyError(caught instanceof Error ? caught.message : "暂时无法读取云端数据"));
|
|
}
|
|
} finally {
|
|
if (redirectedToLogin) return;
|
|
signal.removeEventListener("abort", onParentAbort);
|
|
signal.removeEventListener("abort", clearTimeoutOnParentAbort);
|
|
window.clearTimeout(bootstrapTimeout);
|
|
if (!controller.signal.aborted) {
|
|
if (bootstrapFailed) deps.commit.setHydrated(true);
|
|
else deps.commit.setBootstrapPhase("prepare");
|
|
}
|
|
}
|
|
}
|