From 0d0dabaacefdf0a2874e1f994ba3c835a6ab1633 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 18 Jun 2026 11:58:50 +0800 Subject: [PATCH 1/2] refactor(utils): add barrel exports --- barrelsby.json | 4 +--- src/app/chat/chat-screen.tsx | 4 +--- src/app/chat/components/browser-hint-overlay.tsx | 2 +- src/app/chat/components/pwa-install-overlay.tsx | 5 +---- src/app/splash/splash-screen.tsx | 2 +- src/core/net/config/api_config.ts | 2 +- .../net/interceptor/auth_refresh_interceptor.ts | 3 +-- src/data/dto/auth/index.ts | 2 +- src/data/dto/chat/guest_chat_quota.ts | 2 +- src/data/repositories/auth_repository.ts | 2 +- src/data/repositories/chat_repository.ts | 2 +- .../repositories/interfaces/iauth_repository.ts | 2 +- .../repositories/interfaces/ichat_repository.ts | 2 +- .../interfaces/imetrics_repository.ts | 2 +- .../repositories/interfaces/iuser_repository.ts | 2 +- src/data/repositories/metrics_repository.ts | 2 +- src/data/repositories/user_repository.ts | 2 +- src/data/storage/app/app_storage.ts | 3 +-- src/data/storage/auth/auth_storage.ts | 3 +-- src/data/storage/auth/iauth_storage.ts | 2 +- .../storage/chat/__tests__/chat_storage.test.ts | 3 +-- src/data/storage/chat/chat_storage.ts | 4 +--- src/data/storage/chat/ichat_storage.ts | 2 +- src/data/storage/chat/local_chat_storage.ts | 2 +- src/data/storage/user/iuser_storage.ts | 2 +- src/data/storage/user/user_storage.ts | 3 +-- src/integrations/media-recorder.ts | 2 +- src/stores/auth/auth-actors.ts | 5 +---- src/stores/chat/chat-machine.actors.ts | 2 +- src/stores/chat/chat-machine.helpers.ts | 3 +-- src/stores/chat/chat-machine.ts | 2 +- src/stores/user/user-machine.actors.ts | 2 +- src/stores/user/user-machine.helpers.ts | 2 +- src/utils/index.ts | 15 +++++++++++++++ 34 files changed, 48 insertions(+), 51 deletions(-) create mode 100644 src/utils/index.ts diff --git a/barrelsby.json b/barrelsby.json index 488172d2..1ae7e1e5 100644 --- a/barrelsby.json +++ b/barrelsby.json @@ -30,9 +30,7 @@ "./src/app/subscription/components", "./src/hooks", "./src/integrations", - "./src/models/auth", - "./src/models/chat", - "./src/models/user", + "./src/utils", "./src/stores/auth", "./src/stores/chat", "./src/stores/sidebar", diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index cec8fb9d..bb92d3a7 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -26,9 +26,7 @@ import type { LoginStatus } from "@/data/dto/auth"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; -import { AppEnvUtil } from "@/utils/app-env"; -import { BrowserDetector } from "@/utils/browser-detect"; -import { UrlLauncherUtil } from "@/utils/url-launcher-util"; +import { AppEnvUtil, BrowserDetector, UrlLauncherUtil } from "@/utils"; import { ROUTE_BUILDERS, ROUTES } from "@/router/routes"; import { MobileShell } from "@/app/_components/core"; diff --git a/src/app/chat/components/browser-hint-overlay.tsx b/src/app/chat/components/browser-hint-overlay.tsx index 1cb82743..066b41f8 100644 --- a/src/app/chat/components/browser-hint-overlay.tsx +++ b/src/app/chat/components/browser-hint-overlay.tsx @@ -14,7 +14,7 @@ */ import { useState } from "react"; -import { BrowserDetector } from "@/utils/browser-detect"; +import { BrowserDetector } from "@/utils"; import styles from "./browser-hint-overlay.module.css"; diff --git a/src/app/chat/components/pwa-install-overlay.tsx b/src/app/chat/components/pwa-install-overlay.tsx index fdeac0ad..477a7a0d 100644 --- a/src/app/chat/components/pwa-install-overlay.tsx +++ b/src/app/chat/components/pwa-install-overlay.tsx @@ -14,10 +14,7 @@ */ import { useEffect } from "react"; -import { BrowserDetector } from "@/utils/browser-detect"; -import { AppEnvUtil } from "@/utils/app-env"; -import { SpAsyncUtil } from "@/utils/storage"; -import { pwaUtil } from "@/utils/pwa"; +import { BrowserDetector, AppEnvUtil, SpAsyncUtil, pwaUtil } from "@/utils"; import { PwaInstallDialog } from "./pwa-install-dialog"; import styles from "./pwa-install-overlay.module.css"; diff --git a/src/app/splash/splash-screen.tsx b/src/app/splash/splash-screen.tsx index ac19cd2b..e4883c48 100644 --- a/src/app/splash/splash-screen.tsx +++ b/src/app/splash/splash-screen.tsx @@ -8,7 +8,7 @@ import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; import { useUserDispatch } from "@/stores/user/user-context"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { ROUTES } from "@/router/routes"; -import { pwaUtil } from "@/utils/pwa"; +import { pwaUtil } from "@/utils"; import { SplashBackground, diff --git a/src/core/net/config/api_config.ts b/src/core/net/config/api_config.ts index c3ec69a7..85e0f7d4 100644 --- a/src/core/net/config/api_config.ts +++ b/src/core/net/config/api_config.ts @@ -4,7 +4,7 @@ * 通过环境变量配置后端服务地址与超时时间。 * 原始 Dart: lib/core/net/service_manager.dart */ -import { AppEnvUtil, type AppEnv } from "@/utils/app-env"; +import { AppEnvUtil, type AppEnv } from "@/utils"; export type { AppEnv }; diff --git a/src/core/net/interceptor/auth_refresh_interceptor.ts b/src/core/net/interceptor/auth_refresh_interceptor.ts index c0895aec..72a7a025 100644 --- a/src/core/net/interceptor/auth_refresh_interceptor.ts +++ b/src/core/net/interceptor/auth_refresh_interceptor.ts @@ -8,8 +8,7 @@ import type { FetchHook } from "ofetch"; import { ApiPath } from "../../../data/services/api/api_path"; import { ApiError, ErrorCode } from "../../../data/services/api/api_result"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; -import { deviceIdentifier } from "@/utils/device_identifier"; -import { Result } from "@/utils/result"; +import { deviceIdentifier, Result } from "@/utils"; /** * 不需要 auth token 刷新的路径 diff --git a/src/data/dto/auth/index.ts b/src/data/dto/auth/index.ts index 3f018ccf..09a9e406 100644 --- a/src/data/dto/auth/index.ts +++ b/src/data/dto/auth/index.ts @@ -11,9 +11,9 @@ export * from "./fb_id_login_request"; export * from "./google_login_request"; export * from "./guest_login_request"; export * from "./guest_login_response"; -export * from "./login_status"; export * from "./login_request"; export * from "./login_response"; +export * from "./login_status"; export * from "./logout_response"; export * from "./refresh_token_request"; export * from "./refresh_token_response"; diff --git a/src/data/dto/chat/guest_chat_quota.ts b/src/data/dto/chat/guest_chat_quota.ts index 19e63efd..5d10639b 100644 --- a/src/data/dto/chat/guest_chat_quota.ts +++ b/src/data/dto/chat/guest_chat_quota.ts @@ -6,7 +6,7 @@ import { type GuestChatQuotaInput, type GuestChatQuotaData, } from "@/data/schemas/chat/guest_chat_quota"; -import { AppEnvUtil } from "@/utils/app-env"; +import { AppEnvUtil } from "@/utils"; export class GuestChatQuota { // 静态常量 diff --git a/src/data/repositories/auth_repository.ts b/src/data/repositories/auth_repository.ts index 22656eb9..8fbe7524 100644 --- a/src/data/repositories/auth_repository.ts +++ b/src/data/repositories/auth_repository.ts @@ -28,7 +28,7 @@ import { SendCodeRequest, } from "@/data/dto/auth"; import { User } from "@/data/dto/user"; -import { Result } from "@/utils/result"; +import { Result } from "@/utils"; import type { IAuthRepository } from "@/data/repositories/interfaces"; import { AuthStorage, type IAuthStorage } from "@/data/storage/auth"; import { UserStorage, type IUserStorage } from "@/data/storage/user"; diff --git a/src/data/repositories/chat_repository.ts b/src/data/repositories/chat_repository.ts index a3540cfa..7d5224b4 100644 --- a/src/data/repositories/chat_repository.ts +++ b/src/data/repositories/chat_repository.ts @@ -22,7 +22,7 @@ import { ChatSendResponse, SendMessageRequest, } from "@/data/dto/chat"; -import { Result } from "@/utils/result"; +import { Result } from "@/utils"; import type { IChatRepository } from "@/data/repositories/interfaces"; import { LocalChatStorage, LocalMessage } from "@/data/storage/chat"; diff --git a/src/data/repositories/interfaces/iauth_repository.ts b/src/data/repositories/interfaces/iauth_repository.ts index a3038cfb..5764b46a 100644 --- a/src/data/repositories/interfaces/iauth_repository.ts +++ b/src/data/repositories/interfaces/iauth_repository.ts @@ -13,7 +13,7 @@ * 原始 Dart: lib/data/repositories/auth_repository_impl.dart */ -import type { Result } from "@/utils/result"; +import type { Result } from "@/utils"; import type { GuestLoginResponse, LoginResponse, diff --git a/src/data/repositories/interfaces/ichat_repository.ts b/src/data/repositories/interfaces/ichat_repository.ts index 5ba54115..2074ea88 100644 --- a/src/data/repositories/interfaces/ichat_repository.ts +++ b/src/data/repositories/interfaces/ichat_repository.ts @@ -11,7 +11,7 @@ * 原始 Dart: lib/data/repositories/chat_repository_impl.dart */ -import type { Result } from "@/utils/result"; +import type { Result } from "@/utils"; import type { ChatHistoryResponse, ChatMessage, diff --git a/src/data/repositories/interfaces/imetrics_repository.ts b/src/data/repositories/interfaces/imetrics_repository.ts index 070289c8..45e95f68 100644 --- a/src/data/repositories/interfaces/imetrics_repository.ts +++ b/src/data/repositories/interfaces/imetrics_repository.ts @@ -9,7 +9,7 @@ * 原始 Dart: lib/data/repositories/metrics_repository_impl.dart */ -import type { Result } from "@/utils/result"; +import type { Result } from "@/utils"; export interface IMetricsRepository { /** 上报 PWA 事件。自动注入当前秒级时间戳。 */ diff --git a/src/data/repositories/interfaces/iuser_repository.ts b/src/data/repositories/interfaces/iuser_repository.ts index 185bd659..ba2d96b6 100644 --- a/src/data/repositories/interfaces/iuser_repository.ts +++ b/src/data/repositories/interfaces/iuser_repository.ts @@ -9,7 +9,7 @@ * 原始 Dart: lib/data/repositories/user_repository_impl.dart */ -import type { Result } from "@/utils/result"; +import type { Result } from "@/utils"; import type { CreditsData, CreditsHistoryData, diff --git a/src/data/repositories/metrics_repository.ts b/src/data/repositories/metrics_repository.ts index ab9c0703..d0b4bbf4 100644 --- a/src/data/repositories/metrics_repository.ts +++ b/src/data/repositories/metrics_repository.ts @@ -9,7 +9,7 @@ */ import { MetricsApi, metricsApi } from "@/data/services/api"; import { AppEvent, PwaEvent } from "@/data/dto/metrics"; -import { Result } from "@/utils/result"; +import { Result } from "@/utils"; import type { IMetricsRepository } from "@/data/repositories/interfaces"; export class MetricsRepository implements IMetricsRepository { diff --git a/src/data/repositories/user_repository.ts b/src/data/repositories/user_repository.ts index 3e17bda1..ed039e6f 100644 --- a/src/data/repositories/user_repository.ts +++ b/src/data/repositories/user_repository.ts @@ -16,7 +16,7 @@ import { User, UserStatsResponse, } from "@/data/dto/user"; -import { Result } from "@/utils/result"; +import { Result } from "@/utils"; import type { IUserRepository } from "@/data/repositories/interfaces"; export class UserRepository implements IUserRepository { diff --git a/src/data/storage/app/app_storage.ts b/src/data/storage/app/app_storage.ts index f823eae8..5adf64e4 100644 --- a/src/data/storage/app/app_storage.ts +++ b/src/data/storage/app/app_storage.ts @@ -16,8 +16,7 @@ * - `recordXxx(today)` 写入 todayString,后续 `canXxx` 在同一天返回 `false`。 */ -import { Result, type Result as ResultT } from "@/utils/result"; -import { SpAsyncUtil } from "@/utils/storage"; +import { Result, type Result as ResultT, SpAsyncUtil } from "@/utils"; import { StorageKeys } from "../storage_keys"; export class AppStorage { diff --git a/src/data/storage/auth/auth_storage.ts b/src/data/storage/auth/auth_storage.ts index 872c2b63..60f2307c 100644 --- a/src/data/storage/auth/auth_storage.ts +++ b/src/data/storage/auth/auth_storage.ts @@ -14,8 +14,7 @@ * - `clearAuthData` 顺序清理:loginToken → guestToken → refreshToken,任一失败立即返回 */ -import { Result, type Result as ResultT } from "@/utils/result"; -import { SpAsyncUtil } from "@/utils/storage"; +import { Result, type Result as ResultT, SpAsyncUtil } from "@/utils"; import { StorageKeys } from "../storage_keys"; import type { IAuthStorage } from "./iauth_storage"; diff --git a/src/data/storage/auth/iauth_storage.ts b/src/data/storage/auth/iauth_storage.ts index 871e3630..11cae11d 100644 --- a/src/data/storage/auth/iauth_storage.ts +++ b/src/data/storage/auth/iauth_storage.ts @@ -9,7 +9,7 @@ * - 所有方法返回 `Promise>`,与 Dart `Future>` 对齐 */ -import type { Result } from "@/utils/result"; +import type { Result } from "@/utils"; export interface IAuthStorage { getLoginToken(): Promise>; diff --git a/src/data/storage/chat/__tests__/chat_storage.test.ts b/src/data/storage/chat/__tests__/chat_storage.test.ts index fa642548..b3207491 100644 --- a/src/data/storage/chat/__tests__/chat_storage.test.ts +++ b/src/data/storage/chat/__tests__/chat_storage.test.ts @@ -4,8 +4,7 @@ import memoryDriver from "unstorage/drivers/memory"; import { GuestChatQuota } from "@/data/dto/chat/guest_chat_quota"; import { StorageKeys } from "@/data/storage/storage_keys"; -import { todayString } from "@/utils/date"; -import { SpAsyncUtil } from "@/utils/storage"; +import { todayString, SpAsyncUtil } from "@/utils"; import { ChatStorage } from "../chat_storage"; diff --git a/src/data/storage/chat/chat_storage.ts b/src/data/storage/chat/chat_storage.ts index f455c3a9..2e0f7aa5 100644 --- a/src/data/storage/chat/chat_storage.ts +++ b/src/data/storage/chat/chat_storage.ts @@ -7,9 +7,7 @@ import { import { GuestChatQuota as GuestChatQuotaDto } from "@/data/dto/chat/guest_chat_quota"; import { z } from "zod"; -import { type Result as ResultT, Result } from "@/utils/result"; -import { todayString } from "@/utils/date"; -import { SpAsyncUtil } from "@/utils/storage"; +import { type Result as ResultT, Result, todayString, SpAsyncUtil } from "@/utils"; import { StorageKeys } from "../storage_keys"; import type { IChatStorage } from "./ichat_storage"; diff --git a/src/data/storage/chat/ichat_storage.ts b/src/data/storage/chat/ichat_storage.ts index 62a64382..4d743595 100644 --- a/src/data/storage/chat/ichat_storage.ts +++ b/src/data/storage/chat/ichat_storage.ts @@ -11,7 +11,7 @@ * 跨天判断逻辑(`needsReset`)保留在 `GuestChatQuota` 类上。 */ -import type { Result } from "@/utils/result"; +import type { Result } from "@/utils"; import type { GuestChatQuotaData } from "@/data/schemas/chat/guest_chat_quota"; export interface IChatStorage { diff --git a/src/data/storage/chat/local_chat_storage.ts b/src/data/storage/chat/local_chat_storage.ts index a782c56b..306001d2 100644 --- a/src/data/storage/chat/local_chat_storage.ts +++ b/src/data/storage/chat/local_chat_storage.ts @@ -15,7 +15,7 @@ * 数据量大时考虑升级 schema 加索引。 */ -import { Result, type Result as ResultT } from "@/utils/result"; +import { Result, type Result as ResultT } from "@/utils"; import { LocalChatDB } from "./local_chat_db"; import { LocalMessage } from "./local_message"; diff --git a/src/data/storage/user/iuser_storage.ts b/src/data/storage/user/iuser_storage.ts index c0f7b9ae..8b72f508 100644 --- a/src/data/storage/user/iuser_storage.ts +++ b/src/data/storage/user/iuser_storage.ts @@ -11,7 +11,7 @@ * 具体序列化由实现层通过 Zod schema 校验后转换为 `User` 类实例。 */ -import type { Result } from "@/utils/result"; +import type { Result } from "@/utils"; import type { UserData } from "@/data/schemas/user/user"; export interface IUserStorage { diff --git a/src/data/storage/user/user_storage.ts b/src/data/storage/user/user_storage.ts index eb9280ac..fb079eda 100644 --- a/src/data/storage/user/user_storage.ts +++ b/src/data/storage/user/user_storage.ts @@ -16,8 +16,7 @@ */ import { UserSchema, type UserData } from "@/data/schemas/user/user"; -import { type Result as ResultT } from "@/utils/result"; -import { SpAsyncUtil } from "@/utils/storage"; +import { type Result as ResultT, SpAsyncUtil } from "@/utils"; import { StorageKeys } from "../storage_keys"; import type { IUserStorage } from "./iuser_storage"; diff --git a/src/integrations/media-recorder.ts b/src/integrations/media-recorder.ts index 318040fa..4896636f 100644 --- a/src/integrations/media-recorder.ts +++ b/src/integrations/media-recorder.ts @@ -9,7 +9,7 @@ * * 注:Web Speech API 在 Chrome / Edge 完整支持;Safari / Firefox 仅录音。 */ -import { Result } from "@/utils/result"; +import { Result } from "@/utils"; export interface SpeechRecognitionResultEvent { transcript: string; diff --git a/src/stores/auth/auth-actors.ts b/src/stores/auth/auth-actors.ts index f144abc6..fbe3489d 100644 --- a/src/stores/auth/auth-actors.ts +++ b/src/stores/auth/auth-actors.ts @@ -9,10 +9,7 @@ import { authRepository } from "@/data/repositories/auth_repository"; import type { IAuthRepository } from "@/data/repositories/interfaces"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; -import { deviceIdentifier } from "@/utils/device_identifier"; -import { fetchFacebookUserData } from "@/utils/facebook-graph"; -import { Logger } from "@/utils/logger"; -import { Result } from "@/utils/result"; +import { deviceIdentifier, fetchFacebookUserData, Logger, Result } from "@/utils"; import { readGuestId } from "./auth-helpers"; diff --git a/src/stores/chat/chat-machine.actors.ts b/src/stores/chat/chat-machine.actors.ts index cef62d26..7807edeb 100644 --- a/src/stores/chat/chat-machine.actors.ts +++ b/src/stores/chat/chat-machine.actors.ts @@ -5,7 +5,7 @@ import { fromPromise, fromCallback } from "xstate"; import { createChatWebSocket, ChatWebSocket } from "@/core/net/chat-websocket"; -import { Result } from "@/utils/result"; +import { Result } from "@/utils"; import { PAGE_SIZE, diff --git a/src/stores/chat/chat-machine.helpers.ts b/src/stores/chat/chat-machine.helpers.ts index 62cb08ba..49429090 100644 --- a/src/stores/chat/chat-machine.helpers.ts +++ b/src/stores/chat/chat-machine.helpers.ts @@ -25,8 +25,7 @@ import { chatRepository } from "@/data/repositories/chat_repository"; import type { IChatRepository } from "@/data/repositories/interfaces"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; import { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; -import { formatDate } from "@/utils/date"; -import { Result } from "@/utils/result"; +import { formatDate, Result } from "@/utils"; // ============================================================ // Constants diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index 8e6b6816..15788325 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -33,7 +33,7 @@ import { setup, assign } from "xstate"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; -import { formatDate, todayString } from "@/utils/date"; +import { formatDate, todayString } from "@/utils"; import { ChatState, initialState } from "./chat-state"; diff --git a/src/stores/user/user-machine.actors.ts b/src/stores/user/user-machine.actors.ts index 00c6d56d..15c9d714 100644 --- a/src/stores/user/user-machine.actors.ts +++ b/src/stores/user/user-machine.actors.ts @@ -21,7 +21,7 @@ import type { IAuthRepository, IUserRepository, } from "@/data/repositories/interfaces"; -import { Result } from "@/utils/result"; +import { Result } from "@/utils"; import { type InitData, readInitData, toView, userStorage } from "./user-machine.helpers"; diff --git a/src/stores/user/user-machine.helpers.ts b/src/stores/user/user-machine.helpers.ts index eaa9f901..4fe9b073 100644 --- a/src/stores/user/user-machine.helpers.ts +++ b/src/stores/user/user-machine.helpers.ts @@ -15,7 +15,7 @@ import type { UserView } from "@/data/dto/user"; import { UserStorage } from "@/data/storage/user/user_storage"; -import { Result } from "@/utils/result"; +import { Result } from "@/utils"; // ============================================================ // Storage singleton diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..42003f51 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,15 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from "./app-env"; +export * from "./browser-detect"; +export * from "./date"; +export * from "./device_identifier"; +export * from "./facebook-graph"; +export * from "./logger"; +export * from "./platform-detect"; +export * from "./pwa"; +export * from "./result"; +export * from "./storage"; +export * from "./url-launcher-util"; From f600e11d55827e9b0828d6cc8e777a5c0eefb88d Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 18 Jun 2026 12:52:49 +0800 Subject: [PATCH 2/2] refactor(auth): centralize login status sync --- src/app/auth/auth-screen.tsx | 13 +----- src/app/chat/chat-screen.tsx | 72 ++---------------------------- src/app/sidebar/sidebar-screen.tsx | 17 ------- src/app/splash/splash-screen.tsx | 11 +---- src/providers/root-providers.tsx | 3 ++ src/stores/auth/auth-state.ts | 2 +- src/stores/chat/chat-auth-sync.tsx | 72 ++++++++++++++++++++++++++++++ src/stores/chat/chat-events.ts | 4 +- src/stores/chat/chat-machine.ts | 18 +++++--- src/stores/chat/index.ts | 1 + src/stores/user/user-auth-sync.tsx | 30 +++++++------ 11 files changed, 115 insertions(+), 128 deletions(-) create mode 100644 src/stores/chat/chat-auth-sync.tsx diff --git a/src/app/auth/auth-screen.tsx b/src/app/auth/auth-screen.tsx index f7620d1a..a86a8d4b 100644 --- a/src/app/auth/auth-screen.tsx +++ b/src/app/auth/auth-screen.tsx @@ -9,7 +9,6 @@ import { useRouter } from "next/navigation"; import { MobileShell } from "@/app/_components/core"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; -import { useUserDispatch } from "@/stores/user/user-context"; import { ROUTES } from "@/router/routes"; import { AuthBackground, AuthPanel } from "./components"; @@ -18,10 +17,9 @@ import styles from "./components/auth-screen.module.css"; export function AuthScreen() { const state = useAuthState(); const authDispatch = useAuthDispatch(); - const userDispatch = useUserDispatch(); const router = useRouter(); - // 邮箱登录成功 → 通知 User + 跳转 + // 邮箱登录成功 → 跳转;User hydrate 由根级 监听 loginStatus 变化处理。 // 用 pendingRedirect flag(不是 useRef transition detection)—— // re-mount 时 flag 在 auth context 里持久,区分"刚按了"和"re-visit"准确无误。 useEffect(() => { @@ -37,16 +35,9 @@ export function AuthScreen() { "[auth-screen] useEffect → router.replace(/chat) [via pendingRedirect flag]", ); authDispatch({ type: "AuthClearPendingRedirect" }); - userDispatch({ type: "UserInit" }); router.replace(ROUTES.chat); } - }, [ - state.loginStatus, - state.pendingRedirect, - authDispatch, - userDispatch, - router, - ]); + }, [state.loginStatus, state.pendingRedirect, authDispatch, router]); return ( diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index bb92d3a7..af29ff6e 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -10,22 +10,18 @@ * - BrowserHintOverlay:浏览器提示 * * 鉴权解耦(事件驱动): - * - chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus - * - chat-screen 只派 3 个事件: - * - `ChatGuestLogin` → 游客会话(断 WS = 不连) - * - `ChatNonGuestLogin { token }` → 非游客会话(连 WS) - * - `ChatLogout` → 登出(机器自动 cleanup WS actor) - * - chat 机器内部用 fromCallback actor 完整管理 WS 生命周期 + * - chat 机器不感知鉴权 / 不管 WebSocket + * - auth → chat 的生命周期同步由根级 负责 + * - ChatScreen 只派生 UI 展示所需的 isGuest */ import { useEffect, useRef, useState } from "react"; import Image from "next/image"; -import { useRouter } from "next/navigation"; import { useAuthState } from "@/stores/auth/auth-context"; import type { LoginStatus } from "@/data/dto/auth"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; -import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; +import { useChatState } from "@/stores/chat/chat-context"; import { AppEnvUtil, BrowserDetector, UrlLauncherUtil } from "@/utils"; import { ROUTE_BUILDERS, ROUTES } from "@/router/routes"; @@ -50,39 +46,9 @@ function deriveIsGuest(loginStatus: LoginStatus): boolean { return loginStatus === "guest"; } -/** - * 按 loginStatus 派鉴权生命周期事件(chat 机器不感知 loginStatus —— 这层转换在此) - * - * 设计:不直接管理 WS / 不读 AuthStorage(除取 token) - */ -function dispatchAuthLifecycle( - loginStatus: LoginStatus, - chatDispatch: ReturnType, -): void { - if (loginStatus === "guest") { - // 游客:派 ChatGuestLogin(chat 机器不连 WS —— 业务事实"断 WS") - chatDispatch({ type: "ChatGuestLogin" }); - return; - } - if (loginStatus === "notLoggedIn") { - // 登出:派 ChatLogout(chat 机器自动 cleanup WS actor + 清消息) - chatDispatch({ type: "ChatLogout" }); - return; - } - // 非游客:拿 loginToken → 派 ChatNonGuestLogin { token } - void (async () => { - const tokenR = await AuthStorage.getInstance().getLoginToken(); - if (tokenR.success && tokenR.data) { - chatDispatch({ type: "ChatNonGuestLogin", token: tokenR.data }); - } - })(); -} - export function ChatScreen() { - const router = useRouter(); const state = useChatState(); const authState = useAuthState(); - const chatDispatch = useChatDispatch(); const [showExternalBrowserDialog, setShowExternalBrowserDialog] = useState(false); @@ -98,37 +64,7 @@ export function ChatScreen() { const showExhaustedBanner = isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0; - // ───────────────────────────────────────────────────────────── - // loginStatus 变化 → 派对应鉴权生命周期事件 - // - // 规则: - // - AuthInit 未完成 → 等,避免初始 notLoggedIn 把 /chat 变成死状态 - // - AuthInit 完成后仍 "notLoggedIn" → 清 chat 并回 splash - // - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin - // ───────────────────────────────────────────────────────────── - const prevLoginStatusRef = useRef(null); const externalBrowserPromptShownRef = useRef(false); - useEffect(() => { - if (!authState.hasInitialized || authState.isLoading) return; - - const prev = prevLoginStatusRef.current; - if (prev === authState.loginStatus) return; // 登录态没变 - prevLoginStatusRef.current = authState.loginStatus; - - if (authState.loginStatus === "notLoggedIn") { - chatDispatch({ type: "ChatLogout" }); - router.replace(ROUTES.splash); - return; - } - - dispatchAuthLifecycle(authState.loginStatus, chatDispatch); - }, [ - authState.hasInitialized, - authState.isLoading, - authState.loginStatus, - chatDispatch, - router, - ]); useEffect(() => { if (!authState.hasInitialized || authState.isLoading) return; diff --git a/src/app/sidebar/sidebar-screen.tsx b/src/app/sidebar/sidebar-screen.tsx index f65780f9..a8845b96 100644 --- a/src/app/sidebar/sidebar-screen.tsx +++ b/src/app/sidebar/sidebar-screen.tsx @@ -1,11 +1,8 @@ "use client"; -import { useEffect, useRef } from "react"; -import { useRouter } from "next/navigation"; import { signOut } from "next-auth/react"; import { MobileShell, SettingsSection } from "@/app/_components/core"; -import { ROUTES } from "@/router/routes"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; import { useUserDispatch, useUserState } from "@/stores/user/user-context"; @@ -36,22 +33,8 @@ export function SidebarScreen() { const userDispatch = useUserDispatch(); const auth = useAuthState(); const authDispatch = useAuthDispatch(); - const router = useRouter(); - const isLogoutRedirectPendingRef = useRef(false); - - useEffect(() => { - userDispatch({ type: "UserInit" }); - }, [userDispatch]); - - useEffect(() => { - if (!isLogoutRedirectPendingRef.current) return; - if (auth.isLoading || auth.loginStatus !== "notLoggedIn") return; - isLogoutRedirectPendingRef.current = false; - router.replace(ROUTES.splash); - }, [auth.isLoading, auth.loginStatus, router]); const handleLogoutClick = () => { - isLogoutRedirectPendingRef.current = true; userDispatch({ type: "UserClearLocal" }); authDispatch({ type: "AuthLogoutSubmitted" }); void signOut({ redirect: false }); diff --git a/src/app/splash/splash-screen.tsx b/src/app/splash/splash-screen.tsx index e4883c48..b5496703 100644 --- a/src/app/splash/splash-screen.tsx +++ b/src/app/splash/splash-screen.tsx @@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"; import { MobileShell } from "@/app/_components/core"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; -import { useUserDispatch } from "@/stores/user/user-context"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { ROUTES } from "@/router/routes"; import { pwaUtil } from "@/utils"; @@ -27,7 +26,6 @@ export function SplashScreen() { const router = useRouter(); const state = useAuthState(); const authDispatch = useAuthDispatch(); - const userDispatch = useUserDispatch(); const handleSkip = () => { authDispatch({ type: "AuthGuestLoginSubmitted" }); @@ -81,16 +79,9 @@ export function SplashScreen() { "[splash] useEffect ② → router.replace(/chat) [via pendingRedirect flag]", ); authDispatch({ type: "AuthClearPendingRedirect" }); - userDispatch({ type: "UserInit" }); router.replace(ROUTES.chat); } - }, [ - state.loginStatus, - state.pendingRedirect, - authDispatch, - userDispatch, - router, - ]); + }, [state.loginStatus, state.pendingRedirect, authDispatch, router]); return ( diff --git a/src/providers/root-providers.tsx b/src/providers/root-providers.tsx index a90817e6..8ad92a2f 100644 --- a/src/providers/root-providers.tsx +++ b/src/providers/root-providers.tsx @@ -8,6 +8,7 @@ * + AuthStatusChecker(启动时一次:派发 AuthInit) * + OAuthSessionSync (持续监听 NextAuth session → auth machine) * + UserAuthSync (auth 登录态恢复后 hydrate user machine) + * + ChatAuthSync (持续监听 auth loginStatus → chat machine) * * 它们始终挂载,保证各页面直接 `use*State()` / `use*Dispatch()` 即可, * 无需在每个页面单独包裹。 @@ -18,6 +19,7 @@ import type { ReactNode } from "react"; import { AuthProvider } from "@/stores/auth/auth-context"; import { AuthStatusChecker } from "@/stores/auth/auth-status-checker"; import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync"; +import { ChatAuthSync } from "@/stores/chat/chat-auth-sync"; import { ChatProvider } from "@/stores/chat/chat-context"; import { SidebarProvider } from "@/stores/sidebar/sidebar-context"; import { UserAuthSync } from "@/stores/user/user-auth-sync"; @@ -39,6 +41,7 @@ export function RootProviders({ children }: RootProvidersProps) { + {children}
diff --git a/src/stores/auth/auth-state.ts b/src/stores/auth/auth-state.ts index 84397bc1..6a9bda0c 100644 --- a/src/stores/auth/auth-state.ts +++ b/src/stores/auth/auth-state.ts @@ -13,7 +13,7 @@ export interface AuthState { username: string; confirmPassword: string; errorMessage: string | null; - /** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */ + /** 当前登录状态(未登录 / 游客 / OAuth / 邮箱)—— 下游同步器监听此字段的双向变化 */ loginStatus: LoginStatus; /** 启动时的本地登录态恢复是否已经完成 */ hasInitialized: boolean; diff --git a/src/stores/chat/chat-auth-sync.tsx b/src/stores/chat/chat-auth-sync.tsx new file mode 100644 index 00000000..f9f22c08 --- /dev/null +++ b/src/stores/chat/chat-auth-sync.tsx @@ -0,0 +1,72 @@ +"use client"; + +/** + * Auth → Chat 同步器 + * + * Chat machine 不直接感知 Auth machine;这里作为根级桥接器,持续监听 + * loginStatus 变化并派发 chat 鉴权生命周期事件。这样页面组件只负责 UI, + * 不再承担全局状态同步职责。 + */ +import { useEffect, useRef } from "react"; +import { usePathname, useRouter } from "next/navigation"; + +import type { LoginStatus } from "@/data/dto/auth"; +import { AuthStorage } from "@/data/storage/auth/auth_storage"; +import { useAuthState } from "@/stores/auth/auth-context"; +import { PROTECTED_ROUTES, ROUTES, type StaticRoute } from "@/router/routes"; + +import { useChatDispatch } from "./chat-context"; + +function isProtectedPath(pathname: string): boolean { + return PROTECTED_ROUTES.some((route: StaticRoute) => pathname === route); +} + +export function ChatAuthSync() { + const authState = useAuthState(); + const chatDispatch = useChatDispatch(); + const pathname = usePathname(); + const router = useRouter(); + const prevLoginStatusRef = useRef(null); + + useEffect(() => { + if (!authState.hasInitialized || authState.isLoading) return; + + const prev = prevLoginStatusRef.current; + if (prev === authState.loginStatus) return; + prevLoginStatusRef.current = authState.loginStatus; + + if (authState.loginStatus === "notLoggedIn") { + chatDispatch({ type: "ChatLogout" }); + if (isProtectedPath(pathname)) { + router.replace(ROUTES.splash); + } + return; + } + + if (authState.loginStatus === "guest") { + chatDispatch({ type: "ChatGuestLogin" }); + return; + } + + let cancelled = false; + void (async () => { + const tokenR = await AuthStorage.getInstance().getLoginToken(); + if (!cancelled && tokenR.success && tokenR.data) { + chatDispatch({ type: "ChatNonGuestLogin", token: tokenR.data }); + } + })(); + + return () => { + cancelled = true; + }; + }, [ + authState.hasInitialized, + authState.isLoading, + authState.loginStatus, + chatDispatch, + pathname, + router, + ]); + + return null; +} diff --git a/src/stores/chat/chat-events.ts b/src/stores/chat/chat-events.ts index 1d2eee74..1b039c08 100644 --- a/src/stores/chat/chat-events.ts +++ b/src/stores/chat/chat-events.ts @@ -4,7 +4,7 @@ * 事件名与原 Dart ChatEvent 对齐。 * * 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已删。 - * 鉴权状态变化(loginStatus)由 chat-screen 通过 `useAuthState()` 订阅, + * 鉴权状态变化(loginStatus)由 通过 `useAuthState()` 订阅, * chat 机器不感知鉴权。 * * 鉴权生命周期事件(本轮新增): @@ -16,7 +16,7 @@ * chat-screen 不直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。 */ export type ChatEvent = - // 鉴权生命周期(用户显式触发登录后由 chat-screen 派) + // 鉴权生命周期(登录态变化后由 ChatAuthSync 派) | { type: "ChatGuestLogin" } | { type: "ChatNonGuestLogin"; token: string } | { type: "ChatLogout" } diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index 15788325..063db9b0 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -2,8 +2,8 @@ * Chat 状态机(XState v5) * * 鉴权解耦(事件驱动): - * chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus - * chat-screen 只派 3 个事件: + * chat 机器不感知鉴权 / 不管 WebSocket —— 由 派生 loginStatus + * ChatAuthSync 只派 3 个事件: * - `ChatGuestLogin` → 游客会话(断 WS = 不连) * - `ChatNonGuestLogin { token }` → 非游客会话(连 WS) * - `ChatLogout` → 登出(机器自动 cleanup WS actor) @@ -221,6 +221,10 @@ export const chatMachine = setup({ // guestSession(游客会话 —— 不连 WS,2 个 init 任务并行) // ======================================================================== guestSession: { + on: { + ChatLogout: "#chat.idle", + ChatNonGuestLogin: "#chat.userSession", + }, initial: "initializing", states: { initializing: { @@ -311,8 +315,6 @@ export const chatMachine = setup({ }, ], // 删除 ChatLoadMoreHistory handler —— 游客无服务端 history,不支持翻页 - ChatLogout: "#chat.idle", - ChatNonGuestLogin: "#chat.userSession", ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" }, ChatWebSocketError: { actions: "appendSocketErrorMessage" }, ChatWebSocketConnected: { actions: "setWsConnected" }, @@ -355,6 +357,12 @@ export const chatMachine = setup({ // - sendingViaWs 重新声明 `ChatWebSocketError` 加 target: "ready"(带 action) // —— XState v5: child handler 替换 parent;复制 action on: { + ChatLogout: "#chat.idle", + ChatGuestLogin: "#chat.guestSession", + ChatNonGuestLogin: { + target: "#chat.userSession", + reenter: true, + }, ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" }, ChatWebSocketError: { actions: "appendSocketErrorMessage" }, ChatWebSocketConnected: { actions: "setWsConnected" }, @@ -424,8 +432,6 @@ export const chatMachine = setup({ ChatLoadMoreHistory: { target: "loadingMore", }, - ChatLogout: "#chat.idle", - ChatGuestLogin: "#chat.guestSession", ChatQuotaExceeded: { actions: "incrementQuotaExceeded" }, // 注:ChatAISentenceReceived / ChatWebSocketError / ChatWebSocketConnected // 已上提到 userSession.on,子级不再声明 diff --git a/src/stores/chat/index.ts b/src/stores/chat/index.ts index 78cd11ae..57fcea66 100644 --- a/src/stores/chat/index.ts +++ b/src/stores/chat/index.ts @@ -2,6 +2,7 @@ * @file Automatically generated by barrelsby. */ +export * from "./chat-auth-sync"; export * from "./chat-context"; export * from "./chat-events"; export * from "./chat-machine.actors"; diff --git a/src/stores/user/user-auth-sync.tsx b/src/stores/user/user-auth-sync.tsx index 78710f42..3d57041a 100644 --- a/src/stores/user/user-auth-sync.tsx +++ b/src/stores/user/user-auth-sync.tsx @@ -1,35 +1,39 @@ "use client"; /** - * Auth → User 启动同步器 + * Auth → User 同步器 * - * AuthStatusChecker 只负责恢复 loginStatus;头像等登录用户资料由 - * UserStorage 持久化在 user machine 这一侧。冷启动直达 /chat 时, - * 没有经过 splash/auth 的跳转副作用,因此需要在登录态恢复后主动 - * hydrate user store。 + * AuthStatusChecker 只负责恢复 loginStatus;用户资料由 user machine + * 管理。这里持续监听登录态变化: + * - notLoggedIn → 清空本地 user context + * - guest / email / OAuth → 重新 hydrate user store */ import { useEffect, useRef } from "react"; +import type { LoginStatus } from "@/data/dto/auth"; import { useAuthState } from "@/stores/auth/auth-context"; import { useUserDispatch } from "./user-context"; export function UserAuthSync() { - const { loginStatus } = useAuthState(); + const { hasInitialized, isLoading, loginStatus } = useAuthState(); const userDispatch = useUserDispatch(); - const lastInitializedStatusRef = useRef(null); + const prevLoginStatusRef = useRef(null); - //TODO 在测位栏界面监听变化,监听登录状态的变化若发生变化,则需要分发 user init 事件 useEffect(() => { - if (loginStatus === "notLoggedIn" || loginStatus === "guest") { - lastInitializedStatusRef.current = null; + if (!hasInitialized || isLoading) return; + + const prev = prevLoginStatusRef.current; + if (prev === loginStatus) return; + prevLoginStatusRef.current = loginStatus; + + if (loginStatus === "notLoggedIn") { + userDispatch({ type: "UserClearLocal" }); return; } - if (lastInitializedStatusRef.current === loginStatus) return; - lastInitializedStatusRef.current = loginStatus; userDispatch({ type: "UserInit" }); - }, [loginStatus, userDispatch]); + }, [hasInitialized, isLoading, loginStatus, userDispatch]); return null; }