From e3d549d660a9ac05dd4c347a9d75fda9e42b6028 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 11 Jun 2026 16:06:36 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9Asplash=20=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E9=87=8D=E5=AE=9A=E5=90=91=E7=95=8C=E9=9D=A2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/splash/components/splash-button.tsx | 11 ++-------- src/app/splash/components/splash-screen.tsx | 11 ---------- src/router/proxy.ts | 18 +++------------- src/stores/auth/auth-context.tsx | 11 +++++++++- src/stores/auth/auth-machine.ts | 21 +++++++++--------- src/stores/auth/oauth-session-sync.tsx | 24 ++++----------------- 6 files changed, 30 insertions(+), 66 deletions(-) diff --git a/src/app/splash/components/splash-button.tsx b/src/app/splash/components/splash-button.tsx index 3d0afac5..6a72aaef 100644 --- a/src/app/splash/components/splash-button.tsx +++ b/src/app/splash/components/splash-button.tsx @@ -15,8 +15,8 @@ * * 跳转时序: * 1. 派发事件(如 `AuthGuestLoginSubmitted`) - * 2. auth machine 进 loading state → API 调用 → onDone → state.isSuccess = true - * 3. useEffect 监听到 isSuccess → 调 chat/user store 初始化 + router.replace("/chat") + * 2. auth machine 进 loading state → API 调用 → onDone → loginStatus 更新 + * 3. useEffect 监听到 isSuccess (= loginStatus !== "notLoggedIn") → 调 chat/user store 初始化 + router.replace("/chat") */ import { useRouter } from "next/navigation"; import { useEffect, useRef } from "react"; @@ -47,17 +47,10 @@ export function SplashButton() { // 邮箱/OAuth/游客登录成功跳 /chat + 通知 chat/user 初始化 useEffect(() => { - // DEBUG:isSuccess 状态透明化 - console.log("[splash-button] useEffect state.isSuccess", { - isSuccess: state.isSuccess, - wasSuccess: wasSuccess.current, - willRedirect: state.isSuccess && !wasSuccess.current, - }); if (state.isSuccess && !wasSuccess.current) { wasSuccess.current = true; chatDispatch({ type: "ChatAuthStatusChanged" }); userDispatch({ type: "UserInit" }); - console.log("[splash-button] useEffect → router.replace(/chat) [via isSuccess]"); router.replace(ROUTES.chat); } }, [state.isSuccess, chatDispatch, userDispatch, router]); diff --git a/src/app/splash/components/splash-screen.tsx b/src/app/splash/components/splash-screen.tsx index 46d61bbd..56bffa8d 100644 --- a/src/app/splash/components/splash-screen.tsx +++ b/src/app/splash/components/splash-screen.tsx @@ -22,9 +22,6 @@ export function SplashScreen() { // - 都没有 → 留在 splash(**不**自动创建游客账号 —— 见 auth-machine.ts 的 checkAuthStatusActor) // // proxy 读不到 localStorage(Edge runtime),所以这条 fallback 必须在 Client 侧。 - // - // DEBUG:client 端**不**用 Logger(pino 引入会爆 client bundle),改用 console.log - // 临时调试,bug 修完即删。 useEffect(() => { let cancelled = false; void (async () => { @@ -40,15 +37,7 @@ export function SplashScreen() { (loginTok != null && loginTok.length > 0) || (guestTok != null && guestTok.length > 0); - console.log("[splash] useEffect token check", { - loginToken: loginTok ? `${loginTok.slice(0, 8)}...` : null, - guestToken: guestTok ? `${guestTok.slice(0, 8)}...` : null, - hasAny, - willRedirect: hasAny && !cancelled, - }); - if (!cancelled && hasAny) { - console.log("[splash] useEffect → router.replace(/chat)"); router.replace(ROUTES.chat); } })(); diff --git a/src/router/proxy.ts b/src/router/proxy.ts index 9f09a64d..07d46c3d 100644 --- a/src/router/proxy.ts +++ b/src/router/proxy.ts @@ -23,9 +23,6 @@ * - 函数导出名建议为 `proxy`(可默认导出)。 * * 用 `src/` 时 proxy 必须放在 `src/` 内(参见 `src-folder.md`)。 - * - * DEBUG:每条请求都打日志(`hasSession` / `isAuthOnly` / `isProtected` + 决策), - * 用于排查"进入 /splash 后仍自动跳 /chat"类路由 bug。bug 修完可考虑保留 INFO 级别 + 删 DEBUG。 */ import { NextResponse } from "next/server"; @@ -45,7 +42,7 @@ const SESSION_COOKIE_NAMES = [ "__Secure-next-auth.session-token", ]; -/** proxy 入口日志(Node.js runtime 跑,pino 兼容) */ +/** proxy 入口日志(Node.js runtime 跑,pino 兼容)—— 排查 / 监控路由用 */ const log = new Logger("Proxy"); function isAuthOnlyRoute(pathname: string): boolean { @@ -66,17 +63,9 @@ export function proxy(request: NextRequest) { const hasSession = SESSION_COOKIE_NAMES.some( (name) => Boolean(request.cookies.get(name)?.value), ); - const isAuthOnly = isAuthOnlyRoute(pathname); - const isProtected = isProtectedRoute(pathname); - - // DEBUG:每条请求都打(区分 path 1=proxy 跳 vs path 2=splash useEffect 跳) - log.info( - { pathname, hasSession, isAuthOnly, isProtected }, - "[proxy] request", - ); // 已登录访问未登录专属页 → /chat - if (hasSession && isAuthOnly) { + if (hasSession && isAuthOnlyRoute(pathname)) { log.info( { pathname, target: ROUTES.chat, reason: "logged-in → auth-only" }, "[proxy] redirect", @@ -87,7 +76,7 @@ export function proxy(request: NextRequest) { } // 未登录访问登录态专属页 → /splash(首界面) - if (!hasSession && isProtected) { + if (!hasSession && isProtectedRoute(pathname)) { log.info( { pathname, target: ROUTES.splash, reason: "not-logged-in → protected" }, "[proxy] redirect", @@ -97,7 +86,6 @@ export function proxy(request: NextRequest) { return NextResponse.redirect(url, 308); } - log.info({ pathname }, "[proxy] pass-through"); return NextResponse.next(); } diff --git a/src/stores/auth/auth-context.tsx b/src/stores/auth/auth-context.tsx index 363146ce..d68311cb 100644 --- a/src/stores/auth/auth-context.tsx +++ b/src/stores/auth/auth-context.tsx @@ -32,6 +32,14 @@ interface AuthState { confirmPassword: string; isLoading: boolean; errorMessage: string | null; + /** + * 字段名 `isSuccess` 保留(接口兼容),语义改为: + * true = 用户**已登录**(loginStatus ∈ {email, google, facebook, apple, guest}) + * false = 未登录(loginStatus === "notLoggedIn") + * + * 之前用 `state.matches("success")` 当信号 —— 但 status check 流也会误入 + * success,触发 splash-button 误跳 /chat。现改用 `loginStatus` 判定。 + */ isSuccess: boolean; loginStatus: MachineContext["loginStatus"]; } @@ -62,7 +70,8 @@ export function AuthProvider({ children }: AuthProviderProps) { state.matches("loadingOAuth") || state.matches("loadingGuestLogin"), errorMessage: state.context.errorMessage, - isSuccess: state.matches("success"), + // 改用 loginStatus 判断"已登录"(避开 success state 误触发) + isSuccess: state.context.loginStatus !== "notLoggedIn", loginStatus: state.context.loginStatus, }), [state], diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index 3c2e40ba..2908a205 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -150,6 +150,11 @@ const checkAuthStatusActor = fromPromise(async () => { // ============================================================ // Machine +// +// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。 +// **不**再设 `success` state —— 之前曾用 "success" 当"刚成功"信号, +// 但 status check 流也会误入 success,触发 splash-button 误跳 /chat。 +// 现在改用 `loginStatus !== "notLoggedIn"` 作为"已登录"信号(见 auth-context.tsx)。 // ============================================================ export const authMachine = setup({ types: { @@ -220,7 +225,7 @@ export const authMachine = setup({ invoke: { src: "checkAuthStatus", onDone: { - target: "success", + target: "idle", // ← status check 完回 idle(**不**再假冒 success) actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, @@ -241,7 +246,7 @@ export const authMachine = setup({ invoke: { src: "guestLogin", onDone: { - target: "success", + target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, @@ -266,7 +271,7 @@ export const authMachine = setup({ return { email: event.email, password: event.password }; }, onDone: { - target: "success", + target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, @@ -296,7 +301,7 @@ export const authMachine = setup({ }; }, onDone: { - target: "success", + target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, @@ -343,7 +348,7 @@ export const authMachine = setup({ return { idToken: event.idToken }; }, onDone: { - target: "success", + target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, @@ -368,7 +373,7 @@ export const authMachine = setup({ return { accessToken: event.accessToken }; }, onDone: { - target: "success", + target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, @@ -383,10 +388,6 @@ export const authMachine = setup({ }, }, }, - - success: { - // 终态:业务层通过 state.matches("success") 判断 - }, }, }); diff --git a/src/stores/auth/oauth-session-sync.tsx b/src/stores/auth/oauth-session-sync.tsx index 88f74544..8594f07d 100644 --- a/src/stores/auth/oauth-session-sync.tsx +++ b/src/stores/auth/oauth-session-sync.tsx @@ -10,10 +10,10 @@ * → 业务 token 写入本地 storage * * 此组件**无可见 UI**(返回 null),仅作为 NextAuth session 与 auth state - * machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后。 - * - * DEBUG:client 端**不**用 Logger(pino 引入会爆 client bundle),改用 console.log - * 临时调试,bug 修完即删。 + * machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后: + * ← 提供 useAuthState / useAuthDispatch + * ← 用上面两个 hook + * */ import { useEffect } from "react"; import { useSession } from "next-auth/react"; @@ -26,22 +26,6 @@ export function OAuthSessionSync() { const { loginStatus } = useAuthState(); useEffect(() => { - // DEBUG:每条 session 变化都打(区分 OAuth 残留 session 是否触发 sync) - console.log("[OAuthSessionSync] session change", { - status, - hasSession: !!session, - provider: session?.provider, - idToken: session?.idToken ? `${session.idToken.slice(0, 8)}...` : null, - accessToken: session?.accessToken - ? `${session.accessToken.slice(0, 8)}...` - : null, - loginStatus, - willDispatch: - status === "authenticated" && - !!session?.provider && - loginStatus !== session.provider, - }); - // 1) NextAuth 还没就绪 if (status !== "authenticated" || !session) return;