diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 60b59033..47926597 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -9286,3 +9286,19 @@ - 复发自:无 - 修复版本:待发布 +## BUG-599 | 回访登录自动打开生时校正并挤歪首页 + +- 状态:resolved +- 首次发现:2026-09-09 +- 最近更新:2026-09-09 +- 影响面:登录后首页、`resolveBootstrapSessionSelection`、生时校正 Case open、`.welcome` 布局 +- 用户现象:以前登录过的账户一进首页,主栏内容挤到左边;用户未点任何入口就出现「校正服务暂时不可用」。 +- 触发条件:登录落到 `/`(没有 `?c=`)。会话列表第一项是历史生时校正;该会话列表不带消息,校正会话又跳过消息水合。 +- 根因:bootstrap 把 `sessions[0]` 当成当前会话。校正会话会在 prepare 阶段自动 `POST /api/rectification/cases/open`。打开失败时 catch-all 文案是「校正服务暂时不可用」,并画在首页卡片下方。同时 `.conversation.is-empty` 要求 `messagesHydrated`,校正会话保持 false,首页 1040px 容器没有水平居中。 +- 修复:仅当地址栏 `?c=` 指向该校正会话时才自动打开。默认 `/` 落地改到空咨询会话(没有则新建)。`.welcome` / `.starter-list` 增加 `margin-inline: auto`;空校正会话在未打开 surface 时也使用 `is-empty`。 +- 验证:`frontend/tests/home-bootstrap-reveal.test.ts`、`frontend/tests/rectification-surface-contract.test.ts`、`frontend/tests/session-conversation-layout.test.ts`。 +- 防复发:裸 `/` 不得因最近一条历史是生时校正就调用 Case open;首页居中不得依赖校正会话的 `messagesHydrated`。 +- 相关记录:BUG-027、BUG-034 +- 复发自:无 +- 修复版本:待发布 + diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index c347ce86..05749ff4 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1903,11 +1903,13 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class /* Starter workbench */ .welcome { width: min(1040px, 100%); + margin-inline: auto; padding: var(--space-8) 0 var(--space-12); } .starter-list { width: min(1040px, 100%); + margin-inline: auto; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-6); @@ -2229,6 +2231,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .composer-wrap-starter .composer, .composer-wrap-starter .composer-footer { width: min(1040px, 100%); + margin-inline: auto; } @media (min-width: 768px) and (max-width: 900px) { diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 491119e9..cf092267 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -231,7 +231,10 @@ import { bootstrapLoadingCopy, bootstrapPrepareSettled, bootstrapRevealDelayMs, + resolveStarterHomeLandingSessionId, sessionIdsToPrefetch, + shouldAutoOpenRectificationSession, + starterHomeLandingNeedsConsultation, type BootstrapPhase, } from "@/lib/home-bootstrap"; @@ -520,16 +523,19 @@ export default function Home() { return () => window.removeEventListener("popstate", onPopState); }, [hydrated]); - // A rectification session selected at bootstrap (deep link, refresh, the - // most recent session) is opened and hydrated during the prepare phase, so - // the reveal shows the surface itself rather than a plain transcript that is - // swapped out a moment later. After the reveal the same effect serves - // history navigation. + // A rectification session named in `?c=` (refresh, deep link, login return) + // is opened during prepare so the reveal shows the surface. Bare `/` after + // login must not open the latest history item in the background. useEffect(() => { if ((!hydrated && bootstrapPhase === "account") || !account || !modelCatalog - || activeSession?.sessionType !== "birth_time_rectification" + || !activeSession + || !shouldAutoOpenRectificationSession({ + sessionType: activeSession.sessionType, + sessionId: activeSession.id, + search: window.location.search, + }) || activeSession.id === rectificationSessionId || rectificationLoading || rectificationMutationPending @@ -671,7 +677,11 @@ export default function Home() { dailyStarlanguageApplicable: Boolean(accountId) && profileComplete && natalMinuteAvailable, dailyStarlanguageSettled: dailyStarlanguage.kind !== "pending", entrySummarySettled: rectificationEntrySummarySettled, - rectificationApplicable: activeSession?.sessionType === "birth_time_rectification", + rectificationApplicable: shouldAutoOpenRectificationSession({ + sessionType: activeSession?.sessionType, + sessionId: activeSession?.id, + search: typeof window === "undefined" ? "" : window.location.search, + }), rectificationSettled: activeSession?.id === rectificationSessionId || rectificationError !== "" || !profileComplete, @@ -1013,7 +1023,35 @@ export default function Home() { search: window.location.search, storedReturnId: readLoginSessionReturn(), }); - const activeListed = nextSessions.find((session) => session.id === bootstrapSelection.sessionId) + let landingSessionId = resolveStarterHomeLandingSessionId( + nextSessions, + bootstrapSelection.sessionId, + bootstrapSelection.urlAction, + ); + if (starterHomeLandingNeedsConsultation(nextSessions, landingSessionId, bootstrapSelection.urlAction)) { + if (controller.signal.aborted) return; + const homeSession = createSession( + nextModelCatalog?.defaultModelId ?? "", + "consultation", + chartSnapshotForSession("self", [], nextProfile), + ); + if (nextModelCatalog) { + await writeChatSession(homeSession.id, { + title: homeSession.title, + theme: homeSession.theme, + model_id: homeSession.modelId, + messages: [], + session_type: homeSession.sessionType, + rectification_case_id: homeSession.rectificationCaseId, + chart_profile_id: homeSession.chartProfileId, + chart_profile_name: homeSession.chartProfileName, + chart_profile_role: homeSession.chartProfileRole, + }, "create"); + } + nextSessions = [homeSession, ...nextSessions]; + landingSessionId = homeSession.id; + } + const activeListed = nextSessions.find((session) => session.id === landingSessionId) ?? nextSessions[0]; if (activeListed && !activeListed.messagesHydrated && activeListed.sessionType === "consultation") { try { @@ -1035,7 +1073,7 @@ export default function Home() { setStartGreeting(nextProfile.name.trim() ? createStartGreeting(nextProfile.name) : ""); setOnboardingStep(missingProfileStep(nextProfile) ?? "name"); setSessions(nextSessions); - setActiveSessionId(bootstrapSelection.sessionId); + setActiveSessionId(landingSessionId); if (bootstrapSelection.clearStoredReturn) clearLoginSessionReturn(); if (bootstrapSelection.urlAction === "replace-clear") writeSessionUrl(null, "replace"); if (bootstrapSelection.urlAction === "replace-selected") writeSessionUrl(bootstrapSelection.sessionId, "replace"); @@ -1744,7 +1782,7 @@ export default function Home() { {!rectificationSurfaceOpen && ( -