style: remove markdown bold syntax from code comments
Remove `**...**` emphasis markers from inline comments and JSDoc blocks across auth and chat components, machine mappers, and quota helpers. These are plain code comments, not rendered markdown, so the bold syntax was noise. Files touched: - src/app/auth/components/auth-screen.tsx - src/app/chat/components/chat-header.tsx - src/app/chat/components/chat-screen.tsx - chat machine mapper / quota helpers No functional or behavioral changes.
This commit is contained in:
@@ -24,8 +24,8 @@ export function AuthScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 邮箱登录成功 → 通知 User + 跳转
|
// 邮箱登录成功 → 通知 User + 跳转
|
||||||
// 用 **pendingRedirect flag**(不是 useRef transition detection)——
|
// 用 pendingRedirect flag(不是 useRef transition detection)——
|
||||||
// re-mount 时 flag 在 auth context 里**持久**,区分"刚按了"和"re-visit"准确无误。
|
// re-mount 时 flag 在 auth context 里持久,区分"刚按了"和"re-visit"准确无误。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[auth-screen] useEffect (loginStatus + pendingRedirect)", {
|
console.log("[auth-screen] useEffect (loginStatus + pendingRedirect)", {
|
||||||
pending: state.pendingRedirect,
|
pending: state.pendingRedirect,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* - 游客模式(isGuest=true):渲染 `GuestBanner` 提示注册(点击 → /auth)
|
* - 游客模式(isGuest=true):渲染 `GuestBanner` 提示注册(点击 → /auth)
|
||||||
* - 登录模式(isGuest=false):渲染 `MenuButton`(点击 → push 到 /sidebar)
|
* - 登录模式(isGuest=false):渲染 `MenuButton`(点击 → push 到 /sidebar)
|
||||||
*
|
*
|
||||||
* 注意:菜单按钮直接 push 到侧边栏路由,**不**打开下拉菜单
|
* 注意:菜单按钮直接 push 到侧边栏路由,不打开下拉菜单
|
||||||
* (Dart 端 `MenuButton(onTap: _goSidebar)` 行为)。
|
* (Dart 端 `MenuButton(onTap: _goSidebar)` 行为)。
|
||||||
*/
|
*/
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
* 原始 Dart: lib/ui/chat/chat_screen.dart(172 行)
|
* 原始 Dart: lib/ui/chat/chat_screen.dart(172 行)
|
||||||
*
|
*
|
||||||
* 职责(本文件):
|
* 职责(本文件):
|
||||||
* - **订阅 auth 状态机**(loginStatus)—— 派生 isGuest / **派鉴权生命周期事件**给 chat 机器
|
* - 订阅 auth 状态机(loginStatus)—— 派生 isGuest / 派鉴权生命周期事件给 chat 机器
|
||||||
* - 屏幕生命周期事件(init / visible / invisible)
|
* - 屏幕生命周期事件(init / visible / invisible)
|
||||||
* - **不**直接管理 WebSocket / **不**直接读 AuthStorage(除 token 取值)
|
* - 不直接管理 WebSocket / 不直接读 AuthStorage(除 token 取值)
|
||||||
*
|
*
|
||||||
* 子组件职责:
|
* 子组件职责:
|
||||||
* - ChatHeader:顶部栏(游客 banner / 菜单按钮)
|
* - ChatHeader:顶部栏(游客 banner / 菜单按钮)
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
* - PwaInstallOverlay:PWA 安装提示触发器
|
* - PwaInstallOverlay:PWA 安装提示触发器
|
||||||
* - BrowserHintOverlay:浏览器提示
|
* - BrowserHintOverlay:浏览器提示
|
||||||
*
|
*
|
||||||
* **鉴权解耦**(**事件驱动**):
|
* 鉴权解耦(事件驱动):
|
||||||
* - chat 机器**不**感知鉴权 / **不**管 WebSocket —— 由 chat-screen 派生 loginStatus
|
* - chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus
|
||||||
* - chat-screen **只**派 3 个事件:
|
* - chat-screen 只派 3 个事件:
|
||||||
* - `ChatGuestLogin` → 游客会话(**断** WS = 不连)
|
* - `ChatGuestLogin` → 游客会话(断 WS = 不连)
|
||||||
* - `ChatNonGuestLogin { token }` → 非游客会话(**连** WS)
|
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS)
|
||||||
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
||||||
* - chat 机器**内部**用 fromCallback actor 完整管理 WS 生命周期
|
* - chat 机器内部用 fromCallback actor 完整管理 WS 生命周期
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@@ -49,16 +49,16 @@ function deriveIsGuest(loginStatus: LoginStatus): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 loginStatus 派鉴权生命周期事件(chat 机器**不**感知 loginStatus —— 这层转换在此)
|
* 按 loginStatus 派鉴权生命周期事件(chat 机器不感知 loginStatus —— 这层转换在此)
|
||||||
*
|
*
|
||||||
* 设计:**不**直接管理 WS / **不**读 AuthStorage(除取 token)
|
* 设计:不直接管理 WS / 不读 AuthStorage(除取 token)
|
||||||
*/
|
*/
|
||||||
function dispatchAuthLifecycle(
|
function dispatchAuthLifecycle(
|
||||||
loginStatus: LoginStatus,
|
loginStatus: LoginStatus,
|
||||||
chatDispatch: ReturnType<typeof useChatDispatch>,
|
chatDispatch: ReturnType<typeof useChatDispatch>,
|
||||||
): void {
|
): void {
|
||||||
if (loginStatus === "guest") {
|
if (loginStatus === "guest") {
|
||||||
// 游客:派 ChatGuestLogin(chat 机器**不**连 WS —— 业务事实"断 WS")
|
// 游客:派 ChatGuestLogin(chat 机器不连 WS —— 业务事实"断 WS")
|
||||||
chatDispatch({ type: "ChatGuestLogin" });
|
chatDispatch({ type: "ChatGuestLogin" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ export function ChatScreen() {
|
|||||||
const authState = useAuthState();
|
const authState = useAuthState();
|
||||||
const chatDispatch = useChatDispatch();
|
const chatDispatch = useChatDispatch();
|
||||||
|
|
||||||
// isGuest 派生(chat-screen 是**唯一**知道这个值的地方 —— chat 机器无 isGuest 字段)
|
// isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段)
|
||||||
const isGuest = deriveIsGuest(authState.loginStatus);
|
const isGuest = deriveIsGuest(authState.loginStatus);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
@@ -91,7 +91,7 @@ export function ChatScreen() {
|
|||||||
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
||||||
return () => {
|
return () => {
|
||||||
};
|
};
|
||||||
// 注:mount useEffect 故意**不**依赖 loginStatus —— 首次 mount 时机是固定的
|
// 注:mount useEffect 故意不依赖 loginStatus —— 首次 mount 时机是固定的
|
||||||
// ("用户进 /chat 那一刻"),loginStatus 由 splash/auth-screen 提前 settle。
|
// ("用户进 /chat 那一刻"),loginStatus 由 splash/auth-screen 提前 settle。
|
||||||
// loginStatus 后续变化由下方"副作用 ②" 统一处理。
|
// loginStatus 后续变化由下方"副作用 ②" 统一处理。
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -110,7 +110,7 @@ export function ChatScreen() {
|
|||||||
const prev = prevLoginStatusRef.current;
|
const prev = prevLoginStatusRef.current;
|
||||||
prevLoginStatusRef.current = authState.loginStatus;
|
prevLoginStatusRef.current = authState.loginStatus;
|
||||||
if (prev === null) return; // 首次 mount 由 mount useEffect 处理
|
if (prev === null) return; // 首次 mount 由 mount useEffect 处理
|
||||||
if (prev === authState.loginStatus) return; // 登录态**没**变
|
if (prev === authState.loginStatus) return; // 登录态没变
|
||||||
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
||||||
}, [authState.loginStatus, chatDispatch]);
|
}, [authState.loginStatus, chatDispatch]);
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function MessageContent({ content, imageUrl, isFromAI }: MessageContentPr
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
// **关键**:在 row flex 父中撑开(不依赖 content 循环)—— 让 TextBubble 能拿到真实宽度
|
// 关键:在 row flex 父中撑开(不依赖 content 循环)—— 让 TextBubble 能拿到真实宽度
|
||||||
flex: "1 1 0",
|
flex: "1 1 0",
|
||||||
// 防超长 content(图片 / 超长 url)撑爆父
|
// 防超长 content(图片 / 超长 url)撑爆父
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* `redirect` 回调(`app_router.dart:60-78`)。
|
* `redirect` 回调(`app_router.dart:60-78`)。
|
||||||
*
|
*
|
||||||
* 设计:
|
* 设计:
|
||||||
* - **不**走 Server `redirect()`:避免把 fbid 暴露在 URL 里。
|
* - 不走 Server `redirect()`:避免把 fbid 暴露在 URL 里。
|
||||||
* - deviceId 走 `AuthStorage.getInstance().setDeviceId`(与 `token_interceptor.ts` 同源;
|
* - deviceId 走 `AuthStorage.getInstance().setDeviceId`(与 `token_interceptor.ts` 同源;
|
||||||
* 写入键 `cozsweet.deviceId`)。
|
* 写入键 `cozsweet.deviceId`)。
|
||||||
* - fbid 走 `window.localStorage.setItem(StorageKeys.facebookId, ...)`,
|
* - fbid 走 `window.localStorage.setItem(StorageKeys.facebookId, ...)`,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* Next.js 16 全局 helper `PageProps<'/literal'>` 由 `next dev` / `next build` /
|
* Next.js 16 全局 helper `PageProps<'/literal'>` 由 `next dev` / `next build` /
|
||||||
* `next typegen` 生成。
|
* `next typegen` 生成。
|
||||||
*
|
*
|
||||||
* 这里**不**调用 `redirect()`:把 fbid 暴露在 URL 上对用户不友好;
|
* 这里不调用 `redirect()`:把 fbid 暴露在 URL 上对用户不友好;
|
||||||
* 跳转推迟到 Client 完成持久化之后。
|
* 跳转推迟到 Client 完成持久化之后。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Server Component:把访客直接送入 `/splash`,由 splash 页(Client)根据
|
* Server Component:把访客直接送入 `/splash`,由 splash 页(Client)根据
|
||||||
* `authStorage.hasLoginToken()` 决定是否再跳 `/chat`。
|
* `authStorage.hasLoginToken()` 决定是否再跳 `/chat`。
|
||||||
*
|
*
|
||||||
* 这里**不**做鉴权重定向:Server 端读不到 localStorage,且当前没有 HttpOnly
|
* 这里不做鉴权重定向:Server 端读不到 localStorage,且当前没有 HttpOnly
|
||||||
* cookie(见 `src/data/storage/auth/auth_storage.ts` 的 TODO)。
|
* cookie(见 `src/data/storage/auth/auth_storage.ts` 的 TODO)。
|
||||||
* `src/proxy.ts` 已为未来 cookie 化预留了基于 `login_token` cookie 的 short-circuit。
|
* `src/proxy.ts` 已为未来 cookie 化预留了基于 `login_token` cookie 的 short-circuit。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
/**
|
/**
|
||||||
* Splash 底部按钮组
|
* Splash 底部按钮组
|
||||||
*
|
*
|
||||||
* 本组件是 splash 鉴权流程的**纯 UI 入口**:
|
* 本组件是 splash 鉴权流程的纯 UI 入口:
|
||||||
* - 消费 `useAuthState` / `useAuthDispatch`(仅用于 isLoading 显示 + 派事件)
|
* - 消费 `useAuthState` / `useAuthDispatch`(仅用于 isLoading 显示 + 派事件)
|
||||||
* - **Skip 按钮 = 显式游客登录入口**(派发 `AuthGuestLoginSubmitted`)
|
* - Skip 按钮 = 显式游客登录入口(派发 `AuthGuestLoginSubmitted`)
|
||||||
* - 社交登录按钮仅派发 `AuthFacebookLoginSubmitted` 事件,由状态机负责调 NextAuth
|
* - 社交登录按钮仅派发 `AuthFacebookLoginSubmitted` 事件,由状态机负责调 NextAuth
|
||||||
*
|
*
|
||||||
* **派事件策略**(**单**事件):
|
* 派事件策略(单事件):
|
||||||
* - 派业务事件(`AuthGuestLoginSubmitted` / `AuthFacebookLoginSubmitted`)→ 状态机开始跑
|
* - 派业务事件(`AuthGuestLoginSubmitted` / `AuthFacebookLoginSubmitted`)→ 状态机开始跑
|
||||||
* - `pendingRedirect: true` 在**登录成功 onDone** 里**自动**置(auth-machine.ts `onLoginSuccess` helper)
|
* - `pendingRedirect: true` 在登录成功 onDone 里自动置(auth-machine.ts `onLoginSuccess` helper)
|
||||||
* - splash-screen 看到 flag=true + loginStatus 非 notLoggedIn → 跳 /chat
|
* - splash-screen 看到 flag=true + loginStatus 非 notLoggedIn → 跳 /chat
|
||||||
*/
|
*/
|
||||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||||
@@ -35,7 +35,7 @@ export function SplashButton() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{/* Skip 按钮 = 显式游客登录入口(派发 AuthGuestLoginSubmitted,**不**做路由跳转) */}
|
{/* Skip 按钮 = 显式游客登录入口(派发 AuthGuestLoginSubmitted,不做路由跳转) */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSkip}
|
onClick={handleSkip}
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ import localFont from "next/font/local";
|
|||||||
* subset、inline,浏览器不会向 Google 发送请求,也不会产生 layout shift。
|
* subset、inline,浏览器不会向 Google 发送请求,也不会产生 layout shift。
|
||||||
* - 产出的 CSS 变量由 `globals.css` 桥接到 Tailwind(如 `font-sans`、`font-athelas`)。
|
* - 产出的 CSS 变量由 `globals.css` 桥接到 Tailwind(如 `font-sans`、`font-athelas`)。
|
||||||
*
|
*
|
||||||
* 注意:本文件**不**通过 `src/lib/index.ts` 桶导出,避免被误打进客户端 bundle。
|
* 注意:本文件不通过 `src/lib/index.ts` 桶导出,避免被误打进客户端 bundle。
|
||||||
* 引用方式:`import { geistSans, geistMono, athelas } from "@/lib/fonts";`
|
* 引用方式:`import { geistSans, geistMono, athelas } from "@/lib/fonts";`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ const geistMono = Geist_Mono({
|
|||||||
* 原始资源:/Users/chase/Documents/cozsweet/fonts/Athelas-*.ttf
|
* 原始资源:/Users/chase/Documents/cozsweet/fonts/Athelas-*.ttf
|
||||||
* 放置位置:/public/fonts/Athelas-*.ttf
|
* 放置位置:/public/fonts/Athelas-*.ttf
|
||||||
*
|
*
|
||||||
* 路径计算(`next/font/local` 要求相对路径从**调用文件**出发):
|
* 路径计算(`next/font/local` 要求相对路径从调用文件出发):
|
||||||
* src/lib/fonts.ts → ../../public/fonts/...
|
* src/lib/fonts.ts → ../../public/fonts/...
|
||||||
*/
|
*/
|
||||||
const athelas = localFont({
|
const athelas = localFont({
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const onRequestToken: FetchHook = async (ctx) => {
|
|||||||
|
|
||||||
const token = await getAuthToken();
|
const token = await getAuthToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
// ofetch 推荐:改 ctx.options.headers(**不**依赖 ctx.request 是 Request 实例 —— 实例类型不稳定)
|
// ofetch 推荐:改 ctx.options.headers(不依赖 ctx.request 是 Request 实例 —— 实例类型不稳定)
|
||||||
const headers = new Headers(ctx.options.headers);
|
const headers = new Headers(ctx.options.headers);
|
||||||
headers.set("Authorization", `Bearer ${token}`);
|
headers.set("Authorization", `Bearer ${token}`);
|
||||||
ctx.options.headers = headers;
|
ctx.options.headers = headers;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
* 认证仓库:编排远程 API + 本地 AuthStorage + UserStorage。
|
* 认证仓库:编排远程 API + 本地 AuthStorage + UserStorage。
|
||||||
* 行为对齐原 Dart `lib/data/repositories/auth_repository_impl.dart`:
|
* 行为对齐原 Dart `lib/data/repositories/auth_repository_impl.dart`:
|
||||||
* - 登录成功后调 `_saveLoginData` 持久化 token + User
|
* - 登录成功后调 `_saveLoginData` 持久化 token + User
|
||||||
* - 登出先调 API,再**强制**清本地态(无论 API 成败)
|
* - 登出先调 API,再强制清本地态(无论 API 成败)
|
||||||
* - `getCurrentUser` 成功后**尽力**写本地 User 缓存(不因缓存失败影响主流程)
|
* - `getCurrentUser` 成功后尽力写本地 User 缓存(不因缓存失败影响主流程)
|
||||||
* - 写 storage 失败用 `console.warn` 记录,不让本地缓存失败影响主流程
|
* - 写 storage 失败用 `console.warn` 记录,不让本地缓存失败影响主流程
|
||||||
*
|
*
|
||||||
* 原始 Dart: lib/data/repositories/auth_repository_impl.dart
|
* 原始 Dart: lib/data/repositories/auth_repository_impl.dart
|
||||||
@@ -98,7 +98,7 @@ export class AuthRepository implements IAuthRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录:先调 API 注销服务端会话,**无论成败**都清本地登录态。
|
* 退出登录:先调 API 注销服务端会话,无论成败都清本地登录态。
|
||||||
* 强制清本地的逻辑:用户即使在 API 失败时也想从设备上登出。
|
* 强制清本地的逻辑:用户即使在 API 失败时也想从设备上登出。
|
||||||
*/
|
*/
|
||||||
async logout(): Promise<Result<void>> {
|
async logout(): Promise<Result<void>> {
|
||||||
@@ -237,7 +237,7 @@ export class AuthRepository implements IAuthRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前登录用户。成功后**尽力**写本地 User 缓存(offline hydrate),
|
* 获取当前登录用户。成功后尽力写本地 User 缓存(offline hydrate),
|
||||||
* 缓存失败用 `console.warn` 记录,不影响主流程。
|
* 缓存失败用 `console.warn` 记录,不影响主流程。
|
||||||
*/
|
*/
|
||||||
async getCurrentUser(): Promise<Result<User>> {
|
async getCurrentUser(): Promise<Result<User>> {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export interface IAuthRepository {
|
|||||||
sendCode(email: string): Promise<Result<void>>;
|
sendCode(email: string): Promise<Result<void>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录:先调 API 注销服务端会话,**无论成败**都清本地登录态。
|
* 退出登录:先调 API 注销服务端会话,无论成败都清本地登录态。
|
||||||
*/
|
*/
|
||||||
logout(): Promise<Result<void>>;
|
logout(): Promise<Result<void>>;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const ChatSendResponseSchema = z.object({
|
|||||||
currentMood: z.string().default(""),
|
currentMood: z.string().default(""),
|
||||||
messageId: z.string(),
|
messageId: z.string(),
|
||||||
isGuest: z.boolean().default(false),
|
isGuest: z.boolean().default(false),
|
||||||
// 函数式 default —— 每次 parse 时**重新**调 Date.now()(后端不返回 timestamp 时用**当下**时间)
|
// 函数式 default —— 每次 parse 时重新调 Date.now()(后端不返回 timestamp 时用当下时间)
|
||||||
timestamp: z.number().default(() => Date.now()),
|
timestamp: z.number().default(() => Date.now()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class ChatStorage implements IChatStorage {
|
|||||||
// ---- guest daily chat quota ----
|
// ---- guest daily chat quota ----
|
||||||
|
|
||||||
getGuestDailyChatQuota(): Promise<ResultT<GuestChatQuotaData | null>> {
|
getGuestDailyChatQuota(): Promise<ResultT<GuestChatQuotaData | null>> {
|
||||||
// get-or-init:**有**返**有**,**无**则**用** DTO 默认**值**(`threshold` = max per day)**初**始**化**并**返**回**
|
// get-or-init:有返有,无则用 DTO 默认值(`threshold` = max per day)初始化并返回
|
||||||
return SpAsyncUtil.getJson(
|
return SpAsyncUtil.getJson(
|
||||||
StorageKeys.guestChatQuota,
|
StorageKeys.guestChatQuota,
|
||||||
GuestChatQuotaSchema,
|
GuestChatQuotaSchema,
|
||||||
@@ -57,7 +57,7 @@ export class ChatStorage implements IChatStorage {
|
|||||||
today,
|
today,
|
||||||
);
|
);
|
||||||
if (Result.isErr(initResult)) {
|
if (Result.isErr(initResult)) {
|
||||||
return existing; // **初**始**化**失**败** → **返**原**始** null
|
return existing; // 初始化失败 → 返原始 null
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -78,7 +78,7 @@ export class ChatStorage implements IChatStorage {
|
|||||||
// ---- guest total quota ----
|
// ---- guest total quota ----
|
||||||
|
|
||||||
getGuestTotalQuota(): Promise<ResultT<number | null>> {
|
getGuestTotalQuota(): Promise<ResultT<number | null>> {
|
||||||
// get-or-init:**有**返**有**,**无**则**用** DTO 默认**值**(`totalQuotaDefault`)**初**始**化**并**返**回**
|
// get-or-init:有返有,无则用 DTO 默认值(`totalQuotaDefault`)初始化并返回
|
||||||
return SpAsyncUtil.getJson(
|
return SpAsyncUtil.getJson(
|
||||||
StorageKeys.guestTotalQuotaRemaining,
|
StorageKeys.guestTotalQuotaRemaining,
|
||||||
z.number(),
|
z.number(),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const handler = NextAuth({
|
|||||||
// 把 OAuth provider 的 token 塞进 NextAuth session,供前端取用:
|
// 把 OAuth provider 的 token 塞进 NextAuth session,供前端取用:
|
||||||
// - Google: account.id_token → 调后端 /api/auth/google-login 换业务 token
|
// - Google: account.id_token → 调后端 /api/auth/google-login 换业务 token
|
||||||
// - Facebook: account.access_token → 调后端 /api/auth/facebook-login
|
// - Facebook: account.access_token → 调后端 /api/auth/facebook-login
|
||||||
// 仅在**首次 sign-in** 时把 `account` 注入到 JWT;后续请求 account=undefined
|
// 仅在首次 sign-in 时把 `account` 注入到 JWT;后续请求 account=undefined
|
||||||
// (由 NextAuth 控制),所以下面的 `if (account)` 守卫是必要的。
|
// (由 NextAuth 控制),所以下面的 `if (account)` 守卫是必要的。
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account }) {
|
async jwt({ token, account }) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* AuthPlatform 统一 OAuth 登录入口
|
* AuthPlatform 统一 OAuth 登录入口
|
||||||
*
|
*
|
||||||
* **两个**独立方法(**不**再依赖构造器):
|
* 两个独立方法(不再依赖构造器):
|
||||||
* - `googleSignIn()` → 触发 Google OAuth
|
* - `googleSignIn()` → 触发 Google OAuth
|
||||||
* - `facebookSignIn()` → 触发 Facebook OAuth
|
* - `facebookSignIn()` → 触发 Facebook OAuth
|
||||||
*
|
*
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
* 3. NextAuth 写 next-auth.session-token (httpOnly) cookie
|
* 3. NextAuth 写 next-auth.session-token (httpOnly) cookie
|
||||||
* 4. 重定向到 callbackUrl (默认 /chat)
|
* 4. 重定向到 callbackUrl (默认 /chat)
|
||||||
*
|
*
|
||||||
* **不**做任何持久化(不写 unstorage / 不调后端 / 不设自定义 cookie)。
|
* 不做任何持久化(不写 unstorage / 不调后端 / 不设自定义 cookie)。
|
||||||
* 原始 Dart: nextauth-helpers.{googleLogin, facebookLogin}()
|
* 原始 Dart: nextauth-helpers.{googleLogin, facebookLogin}()
|
||||||
*/
|
*/
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 登出(NextAuth 流程触发器)
|
* 登出(NextAuth 流程触发器)
|
||||||
*
|
*
|
||||||
* 调用 `signOut()` 清除 NextAuth session cookie 并重定向。
|
* 调用 `signOut()` 清除 NextAuth session cookie 并重定向。
|
||||||
* **不**做任何持久化(不写 unstorage / 不调后端 / 不设自定义 cookie)。
|
* 不做任何持久化(不写 unstorage / 不调后端 / 不设自定义 cookie)。
|
||||||
* 原始 Dart: nextauth-helpers.logout()
|
* 原始 Dart: nextauth-helpers.logout()
|
||||||
*/
|
*/
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SessionProvider 客户端包装
|
* SessionProvider 客户端包装
|
||||||
*
|
*
|
||||||
* 背景:next-auth v4 的 `SessionProvider`(从 `next-auth/react` 导出)
|
* 背景:next-auth v4 的 `SessionProvider`(从 `next-auth/react` 导出)
|
||||||
* **没有** `"use client"` 指令。这是 v4 的历史遗留(v5 / Auth.js 已修正)。
|
* 没有 `"use client"` 指令。这是 v4 的历史遗留(v5 / Auth.js 已修正)。
|
||||||
*
|
*
|
||||||
* 当 Server Component(如 `src/app/layout.tsx`)直接 import 时,Next.js
|
* 当 Server Component(如 `src/app/layout.tsx`)直接 import 时,Next.js
|
||||||
* 不会自动把它当 client component 渲染,会继续在服务端 tree 里执行,导致
|
* 不会自动把它当 client component 渲染,会继续在服务端 tree 里执行,导致
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// /**
|
// /
|
||||||
// * Router 公共导出
|
// * Router 公共导出
|
||||||
// *
|
// *
|
||||||
// * 集中管理:
|
// * 集中管理:
|
||||||
|
|||||||
+8
-8
@@ -1,4 +1,4 @@
|
|||||||
// /**
|
// /
|
||||||
// * Next.js 16 Proxy(原 `middleware.ts`)
|
// * Next.js 16 Proxy(原 `middleware.ts`)
|
||||||
// *
|
// *
|
||||||
// * 行为(鉴权路由统一在此处理,业务层组件不再做 auth-driven 跳转):
|
// * 行为(鉴权路由统一在此处理,业务层组件不再做 auth-driven 跳转):
|
||||||
@@ -9,17 +9,17 @@
|
|||||||
// * - 其他情况透传(`NextResponse.next()`)
|
// * - 其他情况透传(`NextResponse.next()`)
|
||||||
// *
|
// *
|
||||||
// * 重要约束:
|
// * 重要约束:
|
||||||
// * - **不**做自定义 cookie 持久化:统一用 NextAuth 内置 `next-auth.session-token` /
|
// * - 不做自定义 cookie 持久化:统一用 NextAuth 内置 `next-auth.session-token` /
|
||||||
// * `__Secure-next-auth.session-token`(v5 默认 httpOnly + secure)作信号。
|
// * `__Secure-next-auth.session-token`(v5 默认 httpOnly + secure)作信号。
|
||||||
// * - 真正的鉴权门在 Client 侧 `useSession()`(next-auth/react),用于 UI 状态。
|
// * - 真正的鉴权门在 Client 侧 `useSession()`(next-auth/react),用于 UI 状态。
|
||||||
// * - 路由跳转(**唯一**鉴权跳转点)由本 proxy 集中处理。
|
// * - 路由跳转(唯一鉴权跳转点)由本 proxy 集中处理。
|
||||||
// * - 副作用:邮箱登录用户(不走 NextAuth)不会被本 proxy 识别为已登录。
|
// * - 副作用:邮箱登录用户(不走 NextAuth)不会被本 proxy 识别为已登录。
|
||||||
// * 邮箱登录在后续轮次若要恢复 proxy 行为,需引入 NextAuth EmailProvider 或独立
|
// * 邮箱登录在后续轮次若要恢复 proxy 行为,需引入 NextAuth EmailProvider 或独立
|
||||||
// * email-session 体系。
|
// * email-session 体系。
|
||||||
// *
|
// *
|
||||||
// * Next 16 重要变更(参见 `node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/proxy.md`):
|
// * Next 16 重要变更(参见 `node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/proxy.md`):
|
||||||
// * - 文件从 `middleware.ts` 重命名为 `proxy.ts`;`middleware` 仍可用但已 deprecated。
|
// * - 文件从 `middleware.ts` 重命名为 `proxy.ts`;`middleware` 仍可用但已 deprecated。
|
||||||
// * - 默认 Node.js runtime;**不**支持 `runtime` 配置(设置会抛错)。
|
// * - 默认 Node.js runtime;不支持 `runtime` 配置(设置会抛错)。
|
||||||
// * - 函数导出名建议为 `proxy`(可默认导出)。
|
// * - 函数导出名建议为 `proxy`(可默认导出)。
|
||||||
// *
|
// *
|
||||||
// * 用 `src/` 时 proxy 必须放在 `src/` 内(参见 `src-folder.md`)。
|
// * 用 `src/` 时 proxy 必须放在 `src/` 内(参见 `src-folder.md`)。
|
||||||
@@ -36,13 +36,13 @@
|
|||||||
// ROUTES,
|
// ROUTES,
|
||||||
// } from "@/router/routes";
|
// } from "@/router/routes";
|
||||||
|
|
||||||
// /** NextAuth 内置 session cookie 名(v5 默认 httpOnly) */
|
// / NextAuth 内置 session cookie 名(v5 默认 httpOnly) */
|
||||||
// const SESSION_COOKIE_NAMES = [
|
// const SESSION_COOKIE_NAMES = [
|
||||||
// "next-auth.session-token",
|
// "next-auth.session-token",
|
||||||
// "__Secure-next-auth.session-token",
|
// "__Secure-next-auth.session-token",
|
||||||
// ];
|
// ];
|
||||||
|
|
||||||
// /** proxy 入口日志(Node.js runtime 跑,pino 兼容)—— 排查 / 监控路由用 */
|
// / proxy 入口日志(Node.js runtime 跑,pino 兼容)—— 排查 / 监控路由用 */
|
||||||
// const log = new Logger("Proxy");
|
// const log = new Logger("Proxy");
|
||||||
|
|
||||||
// function isAuthOnlyRoute(pathname: string): boolean {
|
// function isAuthOnlyRoute(pathname: string): boolean {
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
// return (PROTECTED_ROUTES as readonly string[]).includes(pathname);
|
// return (PROTECTED_ROUTES as readonly string[]).includes(pathname);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// /**
|
// /
|
||||||
// * Proxy 入口
|
// * Proxy 入口
|
||||||
// */
|
// */
|
||||||
// export function proxy(request: NextRequest) {
|
// export function proxy(request: NextRequest) {
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
// return NextResponse.next();
|
// return NextResponse.next();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// /**
|
// /
|
||||||
// * 匹配器:排除 API、Next 静态资源、图标与常见静态文件扩展名。
|
// * 匹配器:排除 API、Next 静态资源、图标与常见静态文件扩展名。
|
||||||
// * 不排除 `_next/data`,因为 proxy 文档明确说明"即使在排除列表中 proxy 仍会跑"
|
// * 不排除 `_next/data`,因为 proxy 文档明确说明"即使在排除列表中 proxy 仍会跑"
|
||||||
// * (安全兜底)。
|
// * (安全兜底)。
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
* - 不导出 Client-only 模块(`use-auth-gate` 走独立路径导入),保证 Server bundle 不被污染。
|
* - 不导出 Client-only 模块(`use-auth-gate` 走独立路径导入),保证 Server bundle 不被污染。
|
||||||
* - 动态深链通过 `ROUTE_BUILDERS` 函数生成,避免手拼字符串。
|
* - 动态深链通过 `ROUTE_BUILDERS` 函数生成,避免手拼字符串。
|
||||||
*
|
*
|
||||||
* 注意:proxy 路由跳转逻辑(2026-06-15 迁出),但本文件的**类型化常量**仍在
|
* 注意:proxy 路由跳转逻辑(2026-06-15 迁出),但本文件的类型化常量仍在
|
||||||
* 被 8 个组件(auth-screen / chat-header / DeepLinkPersist /
|
* 被 8 个组件(auth-screen / chat-header / DeepLinkPersist /
|
||||||
* error / not-found / page / sidebar-screen / splash-screen)使用 —— **不能**整体注释。
|
* error / not-found / page / sidebar-screen / splash-screen)使用 —— 不能整体注释。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 静态路由字面量 */
|
/** 静态路由字面量 */
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const syncFacebookBackendActor = fromPromise<
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ========== 显式游客登录 actor(opt-in) ==========
|
// ========== 显式游客登录 actor(opt-in) ==========
|
||||||
// 由 splash UI 派发 `AuthGuestLoginSubmitted` 触发,**不**自动跑。
|
// 由 splash UI 派发 `AuthGuestLoginSubmitted` 触发,不自动跑。
|
||||||
// 取代了之前 checkAuthStatusActor 的 auto-guest-login 行为 —— 避免每次
|
// 取代了之前 checkAuthStatusActor 的 auto-guest-login 行为 —— 避免每次
|
||||||
// 访 splash 都自动创建游客账号污染后端。
|
// 访 splash 都自动创建游客账号污染后端。
|
||||||
//
|
//
|
||||||
@@ -115,7 +115,7 @@ export const guestLoginActor = fromPromise<LoginStatus, void>(async () => {
|
|||||||
console.log("[auth-machine] guestLoginActor SHORT-CIRCUIT (has local guestToken) → return guest");
|
console.log("[auth-machine] guestLoginActor SHORT-CIRCUIT (has local guestToken) → return guest");
|
||||||
return "guest" as LoginStatus;
|
return "guest" as LoginStatus;
|
||||||
}
|
}
|
||||||
// Result.err(_) 时**不**抛 —— 落到 API 路径让真正的错误信号出现
|
// Result.err(_) 时不抛 —— 落到 API 路径让真正的错误信号出现
|
||||||
|
|
||||||
// 2. 没有(或读不出)才走完整流程:拿 deviceId → 调后端
|
// 2. 没有(或读不出)才走完整流程:拿 deviceId → 调后端
|
||||||
const deviceId = await deviceIdentifier.getDeviceId();
|
const deviceId = await deviceIdentifier.getDeviceId();
|
||||||
@@ -138,15 +138,15 @@ export const guestLoginActor = fromPromise<LoginStatus, void>(async () => {
|
|||||||
|
|
||||||
// ========== 启动 / 恢复时检查登录态 actor(只查不创) ==========
|
// ========== 启动 / 恢复时检查登录态 actor(只查不创) ==========
|
||||||
// 由 <AuthStatusChecker /> 在 mount 时派发(一次性)。
|
// 由 <AuthStatusChecker /> 在 mount 时派发(一次性)。
|
||||||
// 流程(**纯只读**):
|
// 流程(纯只读):
|
||||||
// 1. deviceIdentifier.getDeviceId() —— 无则通过 fingerprintjs 生成 + 落盘
|
// 1. deviceIdentifier.getDeviceId() —— 无则通过 fingerprintjs 生成 + 落盘
|
||||||
// (给后续 OAuth/email 登录准备 deviceId,**不**在 splash 首次访问就 auto-create 游客)
|
// (给后续 OAuth/email 登录准备 deviceId,不在 splash 首次访问就 auto-create 游客)
|
||||||
// 2. AuthStorage.getLoginToken() —— 有 → "email"
|
// 2. AuthStorage.getLoginToken() —— 有 → "email"
|
||||||
// 3. AuthStorage.getGuestToken() —— 有 → "guest"
|
// 3. AuthStorage.getGuestToken() —— 有 → "guest"
|
||||||
// 4. 都没有 → "notLoggedIn"(让用户**显式**选 OAuth / Email / Guest 入口)
|
// 4. 都没有 → "notLoggedIn"(让用户显式选 OAuth / Email / Guest 入口)
|
||||||
|
|
||||||
export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
|
export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
|
||||||
// 1. 拿 deviceId(**不**用于 auto-guest-login,仅供后续登录用)
|
// 1. 拿 deviceId(不用于 auto-guest-login,仅供后续登录用)
|
||||||
await deviceIdentifier.getDeviceId();
|
await deviceIdentifier.getDeviceId();
|
||||||
const storage = AuthStorage.getInstance();
|
const storage = AuthStorage.getInstance();
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export type AuthEvent =
|
|||||||
| { type: "AuthModeChanged"; mode: AuthMode }
|
| { type: "AuthModeChanged"; mode: AuthMode }
|
||||||
| { type: "AuthFormCleared" }
|
| { type: "AuthFormCleared" }
|
||||||
| { type: "AuthReset" }
|
| { type: "AuthReset" }
|
||||||
/** 启动 / 恢复时检查登录态 —— 拿 deviceId → 查 token(**不**自动创建游客账号) */
|
/** 启动 / 恢复时检查登录态 —— 拿 deviceId → 查 token(不自动创建游客账号) */
|
||||||
| { type: "AuthStatusCheckSubmitted" }
|
| { type: "AuthStatusCheckSubmitted" }
|
||||||
/** 显式游客登录 —— splash 上点"游客模式"按钮触发(**不**自动) */
|
/** 显式游客登录 —— splash 上点"游客模式"按钮触发(不自动) */
|
||||||
| { type: "AuthGuestLoginSubmitted" }
|
| { type: "AuthGuestLoginSubmitted" }
|
||||||
// 业务事件(提交)
|
// 业务事件(提交)
|
||||||
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
|
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
|
||||||
@@ -35,7 +35,7 @@ export type AuthEvent =
|
|||||||
| { type: "AuthGoogleSyncSubmitted"; idToken: string }
|
| { type: "AuthGoogleSyncSubmitted"; idToken: string }
|
||||||
| { type: "AuthFacebookSyncSubmitted"; accessToken: string }
|
| { type: "AuthFacebookSyncSubmitted"; accessToken: string }
|
||||||
// ──────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
// splash / auth screen 跳完 /chat 后**清**"刚按了"信号(一次性):
|
// splash / auth screen 跳完 /chat 后清"刚按了"信号(一次性):
|
||||||
// `pendingRedirect` 在 login onDone action 里**自动**置 true(不是按钮派的)
|
// `pendingRedirect` 在 login onDone action 里自动置 true(不是按钮派的)
|
||||||
// —— 因此**只**需要 `AuthClearPendingRedirect` 一个事件
|
// —— 因此只需要 `AuthClearPendingRedirect` 一个事件
|
||||||
| { type: "AuthClearPendingRedirect" };
|
| { type: "AuthClearPendingRedirect" };
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ export type { AuthEvent } from "./auth-events";
|
|||||||
//
|
//
|
||||||
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
|
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
|
||||||
//
|
//
|
||||||
// `pendingRedirect: boolean`("用户**显式**触发了登录"信号)——
|
// `pendingRedirect: boolean`("用户显式触发了登录"信号)——
|
||||||
// **在登录成功的 onDone 里自动置 true**:
|
// 在登录成功的 onDone 里自动置 true:
|
||||||
// splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat
|
// splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat
|
||||||
// 跳完派 `AuthClearPendingRedirect` 置 false
|
// 跳完派 `AuthClearPendingRedirect` 置 false
|
||||||
// checkingAuthStatus **不**设 flag —— 被动查 token 不算"用户意图"。
|
// checkingAuthStatus 不设 flag —— 被动查 token 不算"用户意图"。
|
||||||
//
|
//
|
||||||
// 注:onDone 用了**内联**assign(不是 helper function)—— XState v5 的 TypeScript
|
// 注:onDone 用了内联assign(不是 helper function)—— XState v5 的 TypeScript
|
||||||
// 推断对 helper return type 不友好;内联能保留 `state.matches("loadingXxx")` 的类型。
|
// 推断对 helper return type 不友好;内联能保留 `state.matches("loadingXxx")` 的类型。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
export const authMachine = setup({
|
export const authMachine = setup({
|
||||||
@@ -83,7 +83,7 @@ export const authMachine = setup({
|
|||||||
},
|
},
|
||||||
// 启动 / 恢复时检查登录态 —— 由 <AuthStatusChecker /> 派发
|
// 启动 / 恢复时检查登录态 —— 由 <AuthStatusChecker /> 派发
|
||||||
AuthStatusCheckSubmitted: "checkingAuthStatus",
|
AuthStatusCheckSubmitted: "checkingAuthStatus",
|
||||||
// 显式游客登录 —— 由 splash UI 派发(**不**自动)
|
// 显式游客登录 —— 由 splash UI 派发(不自动)
|
||||||
AuthGuestLoginSubmitted: "loadingGuestLogin",
|
AuthGuestLoginSubmitted: "loadingGuestLogin",
|
||||||
AuthEmailLoginSubmitted: "loadingEmailLogin",
|
AuthEmailLoginSubmitted: "loadingEmailLogin",
|
||||||
AuthEmailRegisterSubmitted: "loadingEmailRegister",
|
AuthEmailRegisterSubmitted: "loadingEmailRegister",
|
||||||
@@ -98,7 +98,7 @@ export const authMachine = setup({
|
|||||||
errorMessage: "Apple login not implemented",
|
errorMessage: "Apple login not implemented",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
// splash / auth screen 跳完 /chat 后**清**flag
|
// splash / auth screen 跳完 /chat 后清flag
|
||||||
AuthClearPendingRedirect: {
|
AuthClearPendingRedirect: {
|
||||||
actions: assign({ pendingRedirect: false }),
|
actions: assign({ pendingRedirect: false }),
|
||||||
},
|
},
|
||||||
@@ -110,10 +110,10 @@ export const authMachine = setup({
|
|||||||
invoke: {
|
invoke: {
|
||||||
src: "checkAuthStatus",
|
src: "checkAuthStatus",
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "idle", // ← status check 完回 idle(**不**再假冒 success)
|
target: "idle", // ← status check 完回 idle(不再假冒 success)
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
// **不**置 pendingRedirect —— 状态查询是**被动**读 token,不算"用户意图"
|
// 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图"
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ export interface AuthState {
|
|||||||
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
|
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
|
||||||
loginStatus: LoginStatus;
|
loginStatus: LoginStatus;
|
||||||
/**
|
/**
|
||||||
* **一次性的"刚按了"信号** —— Skip / Facebook / 邮箱登录按钮**刚**派事件 → 置 true
|
* 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮刚派事件 → 置 true
|
||||||
* → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转
|
* → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转
|
||||||
* → 跳完置 false
|
* → 跳完置 false
|
||||||
*
|
*
|
||||||
* 历史:之前用 useRef 做 transition detection,**但** re-mount 时 ref 重置,bug 复发。
|
* 历史:之前用 useRef 做 transition detection,但 re-mount 时 ref 重置,bug 复发。
|
||||||
* 现**在 auth state 里持久**(machine 不重置)—— re-mount 时也能区分"刚按"和"历史"。
|
* 现在 auth state 里持久(machine 不重置)—— re-mount 时也能区分"刚按"和"历史"。
|
||||||
*/
|
*/
|
||||||
pendingRedirect: boolean;
|
pendingRedirect: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* → auth machine 调 `authRepository.googleLogin/facebookLogin` 换业务 token
|
* → auth machine 调 `authRepository.googleLogin/facebookLogin` 换业务 token
|
||||||
* → 业务 token 写入本地 storage
|
* → 业务 token 写入本地 storage
|
||||||
*
|
*
|
||||||
* 此组件**无可见 UI**(返回 null),仅作为 NextAuth session 与 auth state
|
* 此组件无可见 UI(返回 null),仅作为 NextAuth session 与 auth state
|
||||||
* machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后:
|
* machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后:
|
||||||
* <AuthProvider> ← 提供 useAuthState / useAuthDispatch
|
* <AuthProvider> ← 提供 useAuthState / useAuthDispatch
|
||||||
* <OAuthSessionSync /> ← 用上面两个 hook
|
* <OAuthSessionSync /> ← 用上面两个 hook
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
* 兼容性:保持原 `useChatState()` / `useChatDispatch()` API。
|
* 兼容性:保持原 `useChatState()` / `useChatDispatch()` API。
|
||||||
* 内部将 XState 状态机快照映射回原 `ChatState` 形状。
|
* 内部将 XState 状态机快照映射回原 `ChatState` 形状。
|
||||||
*
|
*
|
||||||
* 注:WebSocket 长生命周期 actor **由 chat-screen.tsx 监听 auth 状态机驱动**——
|
* 注:WebSocket 长生命周期 actor 由 chat-screen.tsx 监听 auth 状态机驱动——
|
||||||
* 鉴权解耦(loginStatus 在 auth,chat 机器**不**感知鉴权)。
|
* 鉴权解耦(loginStatus 在 auth,chat 机器不感知鉴权)。
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
@@ -26,7 +26,7 @@ import type { ChatEvent, ChatState as MachineContext } from "./chat-machine";
|
|||||||
/**
|
/**
|
||||||
* 对外暴露的 State 形状
|
* 对外暴露的 State 形状
|
||||||
*
|
*
|
||||||
* **isGuest 字段已删** —— 由 chat-screen 派生自 `auth.loginStatus === "guest"`。
|
* isGuest 字段已删 —— 由 chat-screen 派生自 `auth.loginStatus === "guest"`。
|
||||||
* 消费方(chat-screen)通过 `useAuthState()` 取 loginStatus,本地派生 isGuest。
|
* 消费方(chat-screen)通过 `useAuthState()` 取 loginStatus,本地派生 isGuest。
|
||||||
*/
|
*/
|
||||||
interface ChatState {
|
interface ChatState {
|
||||||
@@ -40,9 +40,9 @@ interface ChatState {
|
|||||||
isLoadingMore: boolean;
|
isLoadingMore: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
historyOffset: number;
|
historyOffset: number;
|
||||||
/** 游客配额加载完成标**志**(**仅** guestSession 期间) —— UI 可**用**此**显**示 loading */
|
/** 游客配额加载完成标志(仅 guestSession 期间) —— UI 可用此显示 loading */
|
||||||
quotaLoaded: boolean;
|
quotaLoaded: boolean;
|
||||||
/** history 初始加**载**完成标**志**(local → network → save 跑**完**) —— UI 可**用**此**显**示 loading */
|
/** history 初始加载完成标志(local → network → save 跑完) —— UI 可用此显示 loading */
|
||||||
historyLoaded: boolean;
|
historyLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,20 @@
|
|||||||
*
|
*
|
||||||
* 事件名与原 Dart ChatEvent 对齐。
|
* 事件名与原 Dart ChatEvent 对齐。
|
||||||
*
|
*
|
||||||
* 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已**删**。
|
* 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已删。
|
||||||
* 鉴权状态变化(loginStatus)由 chat-screen 通过 `useAuthState()` 订阅,
|
* 鉴权状态变化(loginStatus)由 chat-screen 通过 `useAuthState()` 订阅,
|
||||||
* chat 机器**不**感知鉴权。
|
* chat 机器不感知鉴权。
|
||||||
*
|
*
|
||||||
* 鉴权生命周期事件(**本轮新增**):
|
* 鉴权生命周期事件(本轮新增):
|
||||||
* - `ChatGuestLogin`:游客进入 /chat —— 拉本地消息 + 初始化配额(**不**连 WS)
|
* - `ChatGuestLogin`:游客进入 /chat —— 拉本地消息 + 初始化配额(不连 WS)
|
||||||
* - `ChatNonGuestLogin`:非游客进入 /chat —— 拉服务器端首屏 + **连** WS(token 在 payload)
|
* - `ChatNonGuestLogin`:非游客进入 /chat —— 拉服务器端首屏 + 连 WS(token 在 payload)
|
||||||
* - `ChatLogout`:登出 / 切换用户 —— 断 WS + 清消息
|
* - `ChatLogout`:登出 / 切换用户 —— 断 WS + 清消息
|
||||||
*
|
*
|
||||||
* 设计:**所有**WS / 历史 / 配额相关副作用**全部**通过派发事件给 chat 机器处理,
|
* 设计:所有WS / 历史 / 配额相关副作用全部通过派发事件给 chat 机器处理,
|
||||||
* chat-screen **不**直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。
|
* chat-screen 不直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。
|
||||||
*/
|
*/
|
||||||
export type ChatEvent =
|
export type ChatEvent =
|
||||||
// 鉴权生命周期(**用户**显式触发登录后由 chat-screen 派)
|
// 鉴权生命周期(用户显式触发登录后由 chat-screen 派)
|
||||||
| { type: "ChatGuestLogin" }
|
| { type: "ChatGuestLogin" }
|
||||||
| { type: "ChatNonGuestLogin"; token: string }
|
| { type: "ChatNonGuestLogin"; token: string }
|
||||||
| { type: "ChatLogout" }
|
| { type: "ChatLogout" }
|
||||||
@@ -24,7 +24,7 @@ export type ChatEvent =
|
|||||||
| { type: "ChatSendMessage"; content: string }
|
| { type: "ChatSendMessage"; content: string }
|
||||||
| { type: "ChatSendImage"; imageBase64: string }
|
| { type: "ChatSendImage"; imageBase64: string }
|
||||||
| { type: "ChatLoadMoreHistory" }
|
| { type: "ChatLoadMoreHistory" }
|
||||||
// WebSocket / AI 推句(**由** chat 机器内部 actor 派回 —— "机器**自**驱动")
|
// WebSocket / AI 推句(由 chat 机器内部 actor 派回 —— "机器自驱动")
|
||||||
| {
|
| {
|
||||||
type: "ChatAISentenceReceived";
|
type: "ChatAISentenceReceived";
|
||||||
index: number;
|
index: number;
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
* Chat 状态机:4 个 XState actor
|
* Chat 状态机:4 个 XState actor
|
||||||
*
|
*
|
||||||
* 从 `chat-machine.ts` 抽出的"Actors"段:
|
* 从 `chat-machine.ts` 抽出的"Actors"段:
|
||||||
* - `loadQuotaActor`(fromPromise,**仅**游客有意义 —— 拉**日**配 + 总**配**)
|
* - `loadQuotaActor`(fromPromise,仅游客有意义 —— 拉日配 + 总配)
|
||||||
* - `loadHistoryActor`(fromPromise —— local → network → save network to local 3 **步**)
|
* - `loadHistoryActor`(fromPromise —— local → network → save network to local 3 步)
|
||||||
* - `sendMessageHttpActor`(fromPromise,**已** wire —— "一次发送 = 一次返回")
|
* - `sendMessageHttpActor`(fromPromise,已 wire —— "一次发送 = 一次返回")
|
||||||
* - `loadMoreHistoryActor`(fromPromise —— 翻**页**,**不**走 local-first)
|
* - `loadMoreHistoryActor`(fromPromise —— 翻页,不走 local-first)
|
||||||
* - `chatWebSocketActor`(fromCallback,长生命周期 —— **仅** userSession 期间 invoke)
|
* - `chatWebSocketActor`(fromCallback,长生命周期 —— 仅 userSession 期间 invoke)
|
||||||
*
|
*
|
||||||
* 设计:
|
* 设计:
|
||||||
* - 依赖 `chat-machine.helpers.ts`(纯函数 + 数据加载)
|
* - 依赖 `chat-machine.helpers.ts`(纯函数 + 数据加载)
|
||||||
* - 依赖 `createChatWebSocket`(`@/core/net/chat-websocket`)
|
* - 依赖 `createChatWebSocket`(`@/core/net/chat-websocket`)
|
||||||
* - 保留**全链日志**(用户要求"不要将其删除")
|
* - 保留全链日志(用户要求"不要将其删除")
|
||||||
* - 命名约定:`[chat-machine]` 前缀 —— 与 `chat-machine.ts` 的日志对齐
|
* - 命名约定:`[chat-machine]` 前缀 —— 与 `chat-machine.ts` 的日志对齐
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -31,12 +31,12 @@ import {
|
|||||||
import type { ChatEvent } from "./chat-events";
|
import type { ChatEvent } from "./chat-events";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Init 任务 1:拉游客配额(fromPromise,**纯** local)
|
// Init 任务 1:拉游客配额(fromPromise,纯 local)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/**
|
/**
|
||||||
* 拉游客**日**配 + 总**配**(**仅** guestSession 期间 invoke)
|
* 拉游客日配 + 总配(仅 guestSession 期间 invoke)
|
||||||
* - 失**败**返 `{ remaining: 0, total: 0 }`(**不**抛,**让** UI **还**是**能**进** ready)
|
* - 失败返 `{ remaining: 0, total: 0 }`(不抛,让 UI 还是能进 ready)
|
||||||
* - onDone 在 state machine **里**赋 guestRemainingQuota + guestTotalQuota + 设 quotaLoaded: true
|
* - onDone 在 state machine 里赋 guestRemainingQuota + guestTotalQuota + 设 quotaLoaded: true
|
||||||
*/
|
*/
|
||||||
export const loadQuotaActor = fromPromise<{
|
export const loadQuotaActor = fromPromise<{
|
||||||
remaining: number;
|
remaining: number;
|
||||||
@@ -49,13 +49,13 @@ export const loadQuotaActor = fromPromise<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Init 任务 2:拉 history 3 步流(fromPromise,**返**回**最**终**结**果**)
|
// Init 任务 2:拉 history 3 步流(fromPromise,返回最终结果)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/**
|
/**
|
||||||
* local → network → save network to local 3 步流(**返**回** network **最**终** messages**)
|
* local → network → save network to local 3 步流(返回 network 最终 messages)
|
||||||
* - 选**方**案 A(fromPromise):UI **不**会**看**到** local 中**间**态**,**直**接**显**示** network **结**果**
|
* - 选方案 A(fromPromise):UI 不会看到 local 中间态,直接显示 network 结果
|
||||||
* - 内部 3 步**走** helper `readAndSyncHistory`(**日**志**全**在** helper **里**)
|
* - 内部 3 步走 helper `readAndSyncHistory`(日志全在 helper 里)
|
||||||
* - 失**败**返 local(**退**而**求**其**次**)
|
* - 失败返 local(退而求其次)
|
||||||
*/
|
*/
|
||||||
export const loadHistoryActor = fromPromise<{
|
export const loadHistoryActor = fromPromise<{
|
||||||
messages: UiMessage[];
|
messages: UiMessage[];
|
||||||
@@ -81,10 +81,10 @@ export const loadHistoryActor = fromPromise<{
|
|||||||
// HTTP: one-shot Promise
|
// HTTP: one-shot Promise
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/**
|
/**
|
||||||
* HTTP 发送消息(**一次发送 = 一次返回**)
|
* HTTP 发送消息(一次发送 = 一次返回)
|
||||||
* - 后端响应**就**是 AI 回复(`ChatSendResponse.reply: string`)
|
* - 后端响应就是 AI 回复(`ChatSendResponse.reply: string`)
|
||||||
* - **不**再调 `getLocalMessages()`(**多此一举**)
|
* - 不再调 `getLocalMessages()`(多此一举)
|
||||||
* - 返回 `{ reply: UiMessage }` —— `sending.onDone` 把它**追加**到 `context.messages`
|
* - 返回 `{ reply: UiMessage }` —— `sending.onDone` 把它追加到 `context.messages`
|
||||||
*/
|
*/
|
||||||
export const sendMessageHttpActor = fromPromise<
|
export const sendMessageHttpActor = fromPromise<
|
||||||
{ reply: UiMessage },
|
{ reply: UiMessage },
|
||||||
@@ -122,7 +122,7 @@ export const sendMessageHttpActor = fromPromise<
|
|||||||
return { reply };
|
return { reply };
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 翻历史(pagination)—— **不**走 local-first,**纯** server fetch */
|
/** 翻历史(pagination)—— 不走 local-first,纯 server fetch */
|
||||||
export const loadMoreHistoryActor = fromPromise<
|
export const loadMoreHistoryActor = fromPromise<
|
||||||
{ messages: UiMessage[]; hasMore: boolean; newOffset: number },
|
{ messages: UiMessage[]; hasMore: boolean; newOffset: number },
|
||||||
{ offset: number }
|
{ offset: number }
|
||||||
@@ -173,13 +173,13 @@ export const loadMoreHistoryActor = fromPromise<
|
|||||||
// WebSocket: long-lived callback
|
// WebSocket: long-lived callback
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/**
|
/**
|
||||||
* 长生命周期 WebSocket(**仅** `userSession` 期间 invoke)
|
* 长生命周期 WebSocket(仅 `userSession` 期间 invoke)
|
||||||
* - 启动:连 WS,事件 → sendBack 派回机器
|
* - 启动:连 WS,事件 → sendBack 派回机器
|
||||||
* - 停止(cleanup):disconnect
|
* - 停止(cleanup):disconnect
|
||||||
* - **游客**不** invoke 此 actor(业务事实"游客不连 WS")
|
* - 游客不 invoke 此 actor(业务事实"游客不连 WS")
|
||||||
*
|
*
|
||||||
* 之所以用 fromCallback 而非 fromPromise:
|
* 之所以用 fromCallback 而非 fromPromise:
|
||||||
* - WS 是长连接,**不**是一次性 Promise
|
* - WS 是长连接,不是一次性 Promise
|
||||||
* - fromCallback 提供 `sendBack` 回调,actor 内部事件 → 机器
|
* - fromCallback 提供 `sendBack` 回调,actor 内部事件 → 机器
|
||||||
* - cleanup 函数 → XState 在 parent state exit 时自动调用
|
* - cleanup 函数 → XState 在 parent state exit 时自动调用
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,20 +4,20 @@
|
|||||||
* 从 `chat-machine.ts` 抽出的"helpers"段:
|
* 从 `chat-machine.ts` 抽出的"helpers"段:
|
||||||
* - 翻页大小常量
|
* - 翻页大小常量
|
||||||
* - 仓库注入(接口类型 → 单例)
|
* - 仓库注入(接口类型 → 单例)
|
||||||
* - DTO ↔ UiMessage 映射(**纯函数** —— 可单测)
|
* - DTO ↔ UiMessage 映射(纯函数 —— 可单测)
|
||||||
* - Result → number 映射(**纯函数** —— 可单测)
|
* - Result → number 映射(纯函数 —— 可单测)
|
||||||
* - 2 **个**独立 init 任务**的**数据加载**:
|
* - 2 个独立 init 任务的数据加载:
|
||||||
* - `readGuestQuota()` —— 游客**日**配 + 总**配**(**仅**游客有意义,**纯** ChatStorage)
|
* - `readGuestQuota()` —— 游客日配 + 总配(仅游客有意义,纯 ChatStorage)
|
||||||
* - `readAndSyncHistory()` —— local → network → save network to local 3 **步**
|
* - `readAndSyncHistory()` —— local → network → save network to local 3 步
|
||||||
*
|
*
|
||||||
* 设计目标:
|
* 设计目标:
|
||||||
* - helpers **无 XState 依赖**(可独立 import / 测试)
|
* - helpers 无 XState 依赖(可独立 import / 测试)
|
||||||
* - actors 依赖 helpers(import)
|
* - actors 依赖 helpers(import)
|
||||||
* - machine 依赖 actors(import)—— **不**直接依赖 helpers
|
* - machine 依赖 actors(import)—— 不直接依赖 helpers
|
||||||
*
|
*
|
||||||
* 历史:
|
* 历史:
|
||||||
* - `readInitData` + `InitResult` 已**删**(**被** 2 **个**独立 helper **替**换**)
|
* - `readInitData` + `InitResult` 已删(被 2 个独立 helper 替换)
|
||||||
* - `AuthStorage` import **已**删**(**只** `readInitData` 用过,**移**到** `chatInit` actor **上**层**调**用**)
|
* - `AuthStorage` import 已删(只 `readInitData` 用过,移到 `chatInit` actor 上层调用)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { UiMessage } from "@/models/chat/ui-message";
|
import type { UiMessage } from "@/models/chat/ui-message";
|
||||||
@@ -31,7 +31,7 @@ import { Result } from "@/utils/result";
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/** 翻历史每页大小(**仅** `loadMoreHistoryActor` 用) */
|
/** 翻历史每页大小(仅 `loadMoreHistoryActor` 用) */
|
||||||
export const PAGE_SIZE = 20;
|
export const PAGE_SIZE = 20;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -44,7 +44,7 @@ export const chatRepo: IChatRepository = chatRepository;
|
|||||||
// DTO ↔ UiMessage 映射
|
// DTO ↔ UiMessage 映射
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/**
|
/**
|
||||||
* ChatMessage[] → UiMessage[](**纯函数**)
|
* ChatMessage[] → UiMessage[](纯函数)
|
||||||
* - 与 Dart `LocalMessage → ChatMessage` 等价
|
* - 与 Dart `LocalMessage → ChatMessage` 等价
|
||||||
* - 业务事实:`role === "assistant"` → `isFromAI: true`
|
* - 业务事实:`role === "assistant"` → `isFromAI: true`
|
||||||
*/
|
*/
|
||||||
@@ -63,9 +63,9 @@ export function localMessagesToUi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ChatSendResponse → UiMessage(**纯函数**)
|
* ChatSendResponse → UiMessage(纯函数)
|
||||||
* - 业务事实:后端响应**就是** AI 的回复
|
* - 业务事实:后端响应就是 AI 的回复
|
||||||
* - 用后端 `timestamp`(**不**用本地 time)—— 多设备/时区**一致**
|
* - 用后端 `timestamp`(不用本地 time)—— 多设备/时区一致
|
||||||
*/
|
*/
|
||||||
export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
|
export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
|
||||||
return {
|
return {
|
||||||
@@ -78,7 +78,7 @@ export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Result → number 映射
|
// Result → number 映射
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/** 日配额 Result → number(**纯函数**) */
|
/** 日配额 Result → number(纯函数) */
|
||||||
export function mapQuotaResult(
|
export function mapQuotaResult(
|
||||||
r: Result<{ remaining: number } | null>,
|
r: Result<{ remaining: number } | null>,
|
||||||
fallback: number,
|
fallback: number,
|
||||||
@@ -87,7 +87,7 @@ export function mapQuotaResult(
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 总配额 Result → number(**纯函数**) */
|
/** 总配额 Result → number(纯函数) */
|
||||||
export function mapTotalQuotaResult(
|
export function mapTotalQuotaResult(
|
||||||
r: Result<number | null>,
|
r: Result<number | null>,
|
||||||
fallback: number,
|
fallback: number,
|
||||||
@@ -118,26 +118,26 @@ export async function readGuestQuota(): Promise<{
|
|||||||
// Init 任务 2:history 3 步流(local → network → save)—— `loadHistoryActor` 调
|
// Init 任务 2:history 3 步流(local → network → save)—— `loadHistoryActor` 调
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/**
|
/**
|
||||||
* 拉 history **流**程(**每**步**都**记**日**志** —— 用户要求"**不**要**删**除**")
|
* 拉 history 流程(每步都记日志 —— 用户要求"不要删除")
|
||||||
* 1. **读** local(chatRepo.getLocalMessages)→ **返** localMessages
|
* 1. 读 local(chatRepo.getLocalMessages)→ 返 localMessages
|
||||||
* 2. **读** network(chatRepo.getHistory)→ **返** networkMessages
|
* 2. 读 network(chatRepo.getHistory)→ 返 networkMessages
|
||||||
* 3. **用** network **覆**盖** local(chatRepo.saveMessagesToLocal)→ local **被**同步
|
* 3. 用 network 覆盖 local(chatRepo.saveMessagesToLocal)→ local 被同步
|
||||||
*
|
*
|
||||||
* **返**回** network **最**终** messages**(authorititative),**不**返** local(UI **不**需**要**过渡**态**)
|
* 返回 network 最终 messages(authorititative),不返 local(UI 不需要过渡态)
|
||||||
*
|
*
|
||||||
* **保**留**日**志**(**前**缀 `[chat-machine]` 与 chat-machine.actors.ts **对**齐**)
|
* 保留日志(前缀 `[chat-machine]` 与 chat-machine.actors.ts 对齐)
|
||||||
*/
|
*/
|
||||||
export async function readAndSyncHistory(): Promise<{
|
export async function readAndSyncHistory(): Promise<{
|
||||||
messages: UiMessage[]; // network **最**终** messages(authorititative)
|
messages: UiMessage[]; // network 最终 messages(authorititative)
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
newOffset: number;
|
newOffset: number;
|
||||||
localOverwritten: boolean; // true = local **已**被 network **覆**盖**(**写**入** ChatStorage)
|
localOverwritten: boolean; // true = local 已被 network 覆盖(写入 ChatStorage)
|
||||||
/** 真实从 local 读到的消息数(**调**试**用**) */
|
/** 真实从 local 读到的消息数(调试用) */
|
||||||
localCount: number;
|
localCount: number;
|
||||||
/** 真实从 network 读到的消息数(**调**试**用**) */
|
/** 真实从 network 读到的消息数(调试用) */
|
||||||
networkCount: number;
|
networkCount: number;
|
||||||
}> {
|
}> {
|
||||||
// 1. **读** local
|
// 1. 读 local
|
||||||
const localResult = await chatRepo.getLocalMessages();
|
const localResult = await chatRepo.getLocalMessages();
|
||||||
const localMessages =
|
const localMessages =
|
||||||
Result.isOk(localResult) && localResult.data
|
Result.isOk(localResult) && localResult.data
|
||||||
@@ -147,16 +147,16 @@ export async function readAndSyncHistory(): Promise<{
|
|||||||
count: localMessages.length,
|
count: localMessages.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. **读** network
|
// 2. 读 network
|
||||||
const networkResult = await chatRepo.getHistory(PAGE_SIZE, 0);
|
const networkResult = await chatRepo.getHistory(PAGE_SIZE, 0);
|
||||||
if (!Result.isOk(networkResult)) {
|
if (!Result.isOk(networkResult)) {
|
||||||
// network 失**败** —— **返**空 messages(**不**让 UI 卡**住**)
|
// network 失败 —— 返空 messages(不让 UI 卡住)
|
||||||
console.error(
|
console.error(
|
||||||
"[chat-machine] loadHistory NETWORK FAILED",
|
"[chat-machine] loadHistory NETWORK FAILED",
|
||||||
networkResult.success ? null : (networkResult as { error: unknown }).error,
|
networkResult.success ? null : (networkResult as { error: unknown }).error,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
messages: localMessages, // **退**而**求**其**次**:返 local(**不**然**屏**幕**空**)
|
messages: localMessages, // 退而求其次:返 local(不然屏幕空)
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
newOffset: 0,
|
newOffset: 0,
|
||||||
localOverwritten: false,
|
localOverwritten: false,
|
||||||
@@ -169,7 +169,7 @@ export async function readAndSyncHistory(): Promise<{
|
|||||||
count: networkUi.length,
|
count: networkUi.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. **用** network **覆**盖** local("**再**用**网**络**数**据**覆**盖**本**地**数**据"**这**句**的**字**面**实**现**)
|
// 3. 用 network 覆盖 local("再用网络数据覆盖本地数据"这句的字面实现)
|
||||||
const saveResult = await chatRepo.saveMessagesToLocal(networkResult.data.messages);
|
const saveResult = await chatRepo.saveMessagesToLocal(networkResult.data.messages);
|
||||||
const localOverwritten = Result.isOk(saveResult);
|
const localOverwritten = Result.isOk(saveResult);
|
||||||
console.log("[chat-machine] loadHistory SAVE TO LOCAL DONE", {
|
console.log("[chat-machine] loadHistory SAVE TO LOCAL DONE", {
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* Chat 状态机(XState v5)
|
* Chat 状态机(XState v5)
|
||||||
*
|
*
|
||||||
* **鉴权解耦**(**事件驱动**):
|
* 鉴权解耦(事件驱动):
|
||||||
* chat 机器**不**感知鉴权 / **不**管 WebSocket —— 由 chat-screen 派生 loginStatus
|
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus
|
||||||
* chat-screen **只**派 3 个事件:
|
* chat-screen 只派 3 个事件:
|
||||||
* - `ChatGuestLogin` → 游客会话(**断** WS = 不连)
|
* - `ChatGuestLogin` → 游客会话(断 WS = 不连)
|
||||||
* - `ChatNonGuestLogin { token }` → 非游客会话(**连** WS)
|
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS)
|
||||||
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
||||||
*
|
*
|
||||||
* **状态结构**(parent state 模式):
|
* 状态结构(parent state 模式):
|
||||||
* - `idle`:屏没挂 / 登出
|
* - `idle`:屏没挂 / 登出
|
||||||
* - `guestSession`(**parent**):游客会话 —— **不** invoke WS
|
* - `guestSession`(parent):游客会话 —— 不 invoke WS
|
||||||
* - `userSession`(**parent**):非游客会话 —— **invoke WS**(持久)
|
* - `userSession`(parent):非游客会话 —— invoke WS(持久)
|
||||||
*
|
*
|
||||||
* **init 双任务**(**核**心**重**构**):
|
* init 双任务(核心重构):
|
||||||
* - guestSession.initializing:loadQuota + loadHistory **并**行**(`invoke: [...]` **数**组**)
|
* - guestSession.initializing:loadQuota + loadHistory 并行(`invoke: [...]` 数组)
|
||||||
* - userSession.initializing:loadHistory + parent-level chatWebSocket **并**行
|
* - userSession.initializing:loadHistory + parent-level chatWebSocket 并行
|
||||||
* - **每**个**任务**都**是**独**立** actor,**不**互**相**等**待**(**除**非** `always` barrier)
|
* - 每个任务都是独立 actor,不互相等待(除非 `always` barrier)
|
||||||
*
|
*
|
||||||
* **消息发送路径**(**业务事实**):
|
* 消息发送路径(业务事实):
|
||||||
* - `wsConnected: true`(**仅** userSession 期间)→ **走 WS**,**不**消耗次数(**仅**配额规则)
|
* - `wsConnected: true`(仅 userSession 期间)→ 走 WS,不消耗次数(仅配额规则)
|
||||||
* - `wsConnected: false` → **走 HTTP**,消耗次数(**仅**游客有意义 —— 非游客 0-1=max(0,-1)=0 天然 no-op)
|
* - `wsConnected: false` → 走 HTTP,消耗次数(仅游客有意义 —— 非游客 0-1=max(0,-1)=0 天然 no-op)
|
||||||
* - `ChatWebSocketConnected` 事件 → `wsConnected = true`
|
* - `ChatWebSocketConnected` 事件 → `wsConnected = true`
|
||||||
* - `userSession.exit` action → `wsConnected = false`(WS actor cleanup 时**也**会跑 exit)
|
* - `userSession.exit` action → `wsConnected = false`(WS actor cleanup 时也会跑 exit)
|
||||||
*
|
*
|
||||||
* **配额(**仅**游客)**:
|
* 配额(仅游客):
|
||||||
* - 非游客"无消息限制"(业务事实)—— 永不被赋值
|
* - 非游客"无消息限制"(业务事实)—— 永不被赋值
|
||||||
* - `appendUserMessage` 减 `context.wsConnected ? q : Math.max(0, q-1)` —— WS 已连时**不**递减
|
* - `appendUserMessage` 减 `context.wsConnected ? q : Math.max(0, q-1)` —— WS 已连时不递减
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export const chatMachine = setup({
|
|||||||
appendUserMessage: assign(({ context, event }) => {
|
appendUserMessage: assign(({ context, event }) => {
|
||||||
if (event.type !== "ChatSendMessage") return {};
|
if (event.type !== "ChatSendMessage") return {};
|
||||||
|
|
||||||
// 两**个**额度**同**步**更**新**:WS 已**连****不**递减(**实**时**会**话**不**消耗**),HTTP **必**须**减
|
// 两个额度同步更新:WS 已连不递减(实时会话不消耗),HTTP 必须减
|
||||||
const newRemaining = context.wsConnected
|
const newRemaining = context.wsConnected
|
||||||
? context.guestRemainingQuota
|
? context.guestRemainingQuota
|
||||||
: Math.max(0, context.guestRemainingQuota - 1);
|
: Math.max(0, context.guestRemainingQuota - 1);
|
||||||
@@ -83,8 +83,8 @@ export const chatMachine = setup({
|
|||||||
? context.guestTotalQuota
|
? context.guestTotalQuota
|
||||||
: Math.max(0, context.guestTotalQuota - 1);
|
: Math.max(0, context.guestTotalQuota - 1);
|
||||||
|
|
||||||
// 持**久**化**两**个**额度**(** fire-and-forget,**不** await —— assign **是** sync)
|
// 持久化两个额度( fire-and-forget,不 await —— assign 是 sync)
|
||||||
// daily quota 需**要** today **字**符**串**(**让** ChatStorage 跨**天**判**断** reset)
|
// daily quota 需要 today 字符串(让 ChatStorage 跨天判断 reset)
|
||||||
const today = formatDate();
|
const today = formatDate();
|
||||||
ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
|
ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
|
||||||
ChatStorage.getInstance().setGuestTotalQuota(newTotal);
|
ChatStorage.getInstance().setGuestTotalQuota(newTotal);
|
||||||
@@ -117,7 +117,7 @@ export const chatMachine = setup({
|
|||||||
|
|
||||||
appendUserImage: assign(({ context, event }) => {
|
appendUserImage: assign(({ context, event }) => {
|
||||||
if (event.type !== "ChatSendImage") return {};
|
if (event.type !== "ChatSendImage") return {};
|
||||||
// **同** appendUserMessage:两**个**额度**同**步**减
|
// 同 appendUserMessage:两个额度同步减
|
||||||
const newRemaining = context.wsConnected
|
const newRemaining = context.wsConnected
|
||||||
? context.guestRemainingQuota
|
? context.guestRemainingQuota
|
||||||
: Math.max(0, context.guestRemainingQuota - 1);
|
: Math.max(0, context.guestRemainingQuota - 1);
|
||||||
@@ -211,13 +211,13 @@ export const chatMachine = setup({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// guestSession(游客会话 —— **不**连 WS,2 **个** init 任务**并**行**)
|
// guestSession(游客会话 —— 不连 WS,2 个 init 任务并行)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
guestSession: {
|
guestSession: {
|
||||||
initial: "initializing",
|
initial: "initializing",
|
||||||
states: {
|
states: {
|
||||||
initializing: {
|
initializing: {
|
||||||
// "always" barrier:两**个**任务**都**完**成**才**进** ready
|
// "always" barrier:两个任务都完成才进 ready
|
||||||
always: [
|
always: [
|
||||||
{
|
{
|
||||||
target: "ready",
|
target: "ready",
|
||||||
@@ -225,7 +225,7 @@ export const chatMachine = setup({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
invoke: [
|
invoke: [
|
||||||
// 任务 1:加载配额(fromPromise,**返**结果 + onDone 设 flag)
|
// 任务 1:加载配额(fromPromise,返结果 + onDone 设 flag)
|
||||||
{
|
{
|
||||||
id: "loadQuota",
|
id: "loadQuota",
|
||||||
src: "loadQuota",
|
src: "loadQuota",
|
||||||
@@ -237,13 +237,13 @@ export const chatMachine = setup({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
// 失败**也**标 loaded,**不**卡 init(**游**客**可**能 quota 服务**不**可**用**,**让** UI **进** ready)
|
// 失败也标 loaded,不卡 init(游客可能 quota 服务不可用,让 UI 进 ready)
|
||||||
actions: assign({
|
actions: assign({
|
||||||
quotaLoaded: true,
|
quotaLoaded: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 任务 2:拉 history(fromPromise,**返**回**最**终** network messages**)
|
// 任务 2:拉 history(fromPromise,返回最终 network messages)
|
||||||
{
|
{
|
||||||
id: "loadHistory",
|
id: "loadHistory",
|
||||||
src: "loadHistory",
|
src: "loadHistory",
|
||||||
@@ -256,7 +256,7 @@ export const chatMachine = setup({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
// 失败**也**标 loaded,**不**卡 init(**让** UI **还**是**能**进** ready,**屏**幕**可**能**空**)
|
// 失败也标 loaded,不卡 init(让 UI 还是能进 ready,屏幕可能空)
|
||||||
actions: assign({
|
actions: assign({
|
||||||
historyLoaded: true,
|
historyLoaded: true,
|
||||||
}),
|
}),
|
||||||
@@ -266,29 +266,29 @@ export const chatMachine = setup({
|
|||||||
},
|
},
|
||||||
ready: {
|
ready: {
|
||||||
on: {
|
on: {
|
||||||
// **配**额**检**查** + 发**送**消息**(3 **条** guard,**按**顺**序**匹**配**)
|
// 配额检查 + 发送消息(3 条 guard,按顺序匹配)
|
||||||
// 1. **总**配**额**(guestTotalQuota)**不**足 → **触**发** quota exceeded dialog,**不**发**送**
|
// 1. 总配额(guestTotalQuota)不足 → 触发 quota exceeded dialog,不发送
|
||||||
// 2. **每**日**配**额**(guestRemainingQuota)**不**足 → **同**上**
|
// 2. 每日配额(guestRemainingQuota)不足 → 同上
|
||||||
// 3. **两**个**配**额**都** OK + **内**容**非**空** → **发**送** + **减**配**额**
|
// 3. 两个配额都 OK + 内容非空 → 发送 + 减配额
|
||||||
ChatSendMessage: [
|
ChatSendMessage: [
|
||||||
// 1. 总配**额** **不**足**(guestTotalQuota <= 0) → **触**发** quota exceeded dialog
|
// 1. 总配额 不足(guestTotalQuota <= 0) → 触发 quota exceeded dialog
|
||||||
{
|
{
|
||||||
guard: ({ context }) => context.guestTotalQuota <= 0,
|
guard: ({ context }) => context.guestTotalQuota <= 0,
|
||||||
actions: "incrementQuotaExceeded",
|
actions: "incrementQuotaExceeded",
|
||||||
},
|
},
|
||||||
// 2. 每**日**配**额** **不**足**(guestRemainingQuota <= 0) → **触**发** quota exceeded dialog
|
// 2. 每日配额 不足(guestRemainingQuota <= 0) → 触发 quota exceeded dialog
|
||||||
{
|
{
|
||||||
guard: ({ context }) => context.guestRemainingQuota <= 0,
|
guard: ({ context }) => context.guestRemainingQuota <= 0,
|
||||||
actions: "incrementQuotaExceeded",
|
actions: "incrementQuotaExceeded",
|
||||||
},
|
},
|
||||||
// 3. 两**个**配**额**都** OK + **内**容**非**空** → **发**送**(**只**用** appendUserMessage **自**己**的**日**志**)
|
// 3. 两个配额都 OK + 内容非空 → 发送(只用 appendUserMessage 自己的日志)
|
||||||
{
|
{
|
||||||
actions: "appendUserMessage",
|
actions: "appendUserMessage",
|
||||||
guard: ({ event }) => event.content.trim().length > 0,
|
guard: ({ event }) => event.content.trim().length > 0,
|
||||||
target: "sending",
|
target: "sending",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// **配**额**检**查** + 发**送**图**片**(**同**样** 3 **条** guard,**不**检**查** "内**容**空"**)
|
// 配额检查 + 发送图片(同样 3 条 guard,不检查 "内容空")
|
||||||
ChatSendImage: [
|
ChatSendImage: [
|
||||||
{
|
{
|
||||||
guard: ({ context }) => context.guestTotalQuota <= 0,
|
guard: ({ context }) => context.guestTotalQuota <= 0,
|
||||||
@@ -303,7 +303,7 @@ export const chatMachine = setup({
|
|||||||
target: "sending",
|
target: "sending",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// **删**除 ChatLoadMoreHistory handler —— 游客**无**服**务**端** history,**不**支**持**翻**页**
|
// 删除 ChatLoadMoreHistory handler —— 游客无服务端 history,不支持翻页
|
||||||
ChatLogout: "#chat.idle",
|
ChatLogout: "#chat.idle",
|
||||||
ChatNonGuestLogin: "#chat.userSession",
|
ChatNonGuestLogin: "#chat.userSession",
|
||||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||||
@@ -331,18 +331,18 @@ export const chatMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// **删**除 loadingMore(游客**无**翻**页**)
|
// 删除 loadingMore(游客无翻页)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// userSession(非游客会话 —— WS **父**级 + history **子**级**)
|
// userSession(非游客会话 —— WS 父级 + history 子级)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
userSession: {
|
userSession: {
|
||||||
initial: "initializing",
|
initial: "initializing",
|
||||||
exit: "clearWsConnected",
|
exit: "clearWsConnected",
|
||||||
invoke: {
|
invoke: {
|
||||||
// 父级:WS 长**连**接(**一**进**来****就**连,跨 initializing/ready/sending/loadingMore)
|
// 父级:WS 长连接(一进来就连,跨 initializing/ready/sending/loadingMore)
|
||||||
src: "chatWebSocket",
|
src: "chatWebSocket",
|
||||||
input: ({ event }) => ({
|
input: ({ event }) => ({
|
||||||
token: event.type === "ChatNonGuestLogin" ? event.token : "",
|
token: event.type === "ChatNonGuestLogin" ? event.token : "",
|
||||||
@@ -350,7 +350,7 @@ export const chatMachine = setup({
|
|||||||
},
|
},
|
||||||
states: {
|
states: {
|
||||||
initializing: {
|
initializing: {
|
||||||
// barrier:WS **连**上** + history **加**载**完**成**才**进** ready
|
// barrier:WS 连上 + history 加载完成才进 ready
|
||||||
always: [
|
always: [
|
||||||
{
|
{
|
||||||
target: "ready",
|
target: "ready",
|
||||||
@@ -358,7 +358,7 @@ export const chatMachine = setup({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
invoke: {
|
invoke: {
|
||||||
// 任务 2:拉 history(**只**调**用** loadHistoryActor,**不**再**用** loadMoreHistoryActor)
|
// 任务 2:拉 history(只调用 loadHistoryActor,不再用 loadMoreHistoryActor)
|
||||||
src: "loadHistory",
|
src: "loadHistory",
|
||||||
onDone: {
|
onDone: {
|
||||||
actions: assign(({ event }) => ({
|
actions: assign(({ event }) => ({
|
||||||
@@ -370,7 +370,7 @@ export const chatMachine = setup({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
// 失败**也**标 loaded,**不**卡 init
|
// 失败也标 loaded,不卡 init
|
||||||
actions: assign({
|
actions: assign({
|
||||||
isLoadingMore: false,
|
isLoadingMore: false,
|
||||||
historyLoaded: true,
|
historyLoaded: true,
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
* 注:GuestChatQuota 仍位于 chat-machine.ts(属于业务常量,与上下文配对紧密)。
|
* 注:GuestChatQuota 仍位于 chat-machine.ts(属于业务常量,与上下文配对紧密)。
|
||||||
*
|
*
|
||||||
* 历史:
|
* 历史:
|
||||||
* - `isGuest: boolean` 字段已**删**(由 chat-screen 派生自 `auth.loginStatus === "guest"`)
|
* - `isGuest: boolean` 字段已删(由 chat-screen 派生自 `auth.loginStatus === "guest"`)
|
||||||
* - `guestRemainingQuota` / `guestTotalQuota` default **改 0**:
|
* - `guestRemainingQuota` / `guestTotalQuota` default 改 0:
|
||||||
* - 游客:chat-screen 派 `ChatInit` → readInitData 调 ChatStorage → onDone 条件赋
|
* - 游客:chat-screen 派 `ChatInit` → readInitData 调 ChatStorage → onDone 条件赋
|
||||||
* - 非游客:**永不被赋值**(业务事实"非游客无消息限制")—— 0 - 1 = max(0, -1) = 0 天然 no-op
|
* - 非游客:永不被赋值(业务事实"非游客无消息限制")—— 0 - 1 = max(0, -1) = 0 天然 no-op
|
||||||
* - 0 同时表示"未初始化"和"已耗尽" —— UI 用 `if (isGuest && ...)` 天然过滤
|
* - 0 同时表示"未初始化"和"已耗尽" —— UI 用 `if (isGuest && ...)` 天然过滤
|
||||||
*
|
*
|
||||||
* 新增 `wsConnected: boolean`(**仅** userSession 期间 true):
|
* 新增 `wsConnected: boolean`(仅 userSession 期间 true):
|
||||||
* - **WS 已连** → 消息发送走 WS,**不**消耗次数
|
* - WS 已连 → 消息发送走 WS,不消耗次数
|
||||||
* - **WS 未连** → 消息发送走 HTTP,消耗次数
|
* - WS 未连 → 消息发送走 HTTP,消耗次数
|
||||||
* - `ChatWebSocketConnected` 事件 → true
|
* - `ChatWebSocketConnected` 事件 → true
|
||||||
* - `userSession.exit` → false(WS actor cleanup 时**也**会跑 exit)
|
* - `userSession.exit` → false(WS actor cleanup 时也会跑 exit)
|
||||||
*/
|
*/
|
||||||
import type { UiMessage } from "@/models/chat/ui-message";
|
import type { UiMessage } from "@/models/chat/ui-message";
|
||||||
|
|
||||||
@@ -22,13 +22,13 @@ export interface ChatState {
|
|||||||
messages: UiMessage[];
|
messages: UiMessage[];
|
||||||
isReplyingAI: boolean;
|
isReplyingAI: boolean;
|
||||||
/**
|
/**
|
||||||
* WebSocket 连接状态(**仅** userSession 期间 true)
|
* WebSocket 连接状态(仅 userSession 期间 true)
|
||||||
* - 决定消息发送**路径**(WS / HTTP)和**是否消耗**次数
|
* - 决定消息发送路径(WS / HTTP)和是否消耗次数
|
||||||
* - true:走 WS,**不**消耗次数
|
* - true:走 WS,不消耗次数
|
||||||
* - false:走 HTTP,消耗次数
|
* - false:走 HTTP,消耗次数
|
||||||
*/
|
*/
|
||||||
wsConnected: boolean;
|
wsConnected: boolean;
|
||||||
/** 游客剩余配额(次/天)—— **仅**游客业务展示用
|
/** 游客剩余配额(次/天)—— 仅游客业务展示用
|
||||||
*/
|
*/
|
||||||
guestRemainingQuota: number;
|
guestRemainingQuota: number;
|
||||||
/** 游客总配额(仅游客展示用) */
|
/** 游客总配额(仅游客展示用) */
|
||||||
@@ -37,14 +37,14 @@ export interface ChatState {
|
|||||||
isLoadingMore: boolean;
|
isLoadingMore: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
historyOffset: number;
|
historyOffset: number;
|
||||||
/** 游客配额加载完成标**志**(**仅** guestSession 期间) —— default false
|
/** 游客配额加载完成标志(仅 guestSession 期间) —— default false
|
||||||
* - guestSession.initializing **走** always barrier:`quotaLoaded && historyLoaded` **才**进** ready
|
* - guestSession.initializing 走 always barrier:`quotaLoaded && historyLoaded` 才进 ready
|
||||||
* - userSession **永不为** true(**不**调 loadQuota)—— OK,**不**影响 barrier
|
* - userSession 永不为 true(不调 loadQuota)—— OK,不影响 barrier
|
||||||
*/
|
*/
|
||||||
quotaLoaded: boolean;
|
quotaLoaded: boolean;
|
||||||
/** history 加**载**完成标**志**(initial load —— local → network → save 跑**完**)
|
/** history 加载完成标志(initial load —— local → network → save 跑完)
|
||||||
* - guestSession.initializing / userSession.initializing 走 always barrier
|
* - guestSession.initializing / userSession.initializing 走 always barrier
|
||||||
* - **不**被 `loadMoreHistoryActor` 设**置**(**翻**页**是**另**一**码**事**)
|
* - 不被 `loadMoreHistoryActor` 设置(翻页是另一码事)
|
||||||
*/
|
*/
|
||||||
historyLoaded: boolean;
|
historyLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -57,9 +57,9 @@ export const Result = {
|
|||||||
*
|
*
|
||||||
* 错误处理策略(重点):
|
* 错误处理策略(重点):
|
||||||
* 1. 捕获后用 `toError` 规范化(保留 .stack / .code / .status 等字段)
|
* 1. 捕获后用 `toError` 规范化(保留 .stack / .code / .status 等字段)
|
||||||
* 2. **用 Logger 记录**(带 stack + component=Result 标签)—— 调试 dev 控制台可见,
|
* 2. 用 Logger 记录(带 stack + component=Result 标签)—— 调试 dev 控制台可见,
|
||||||
* 生产环境走 JSON pipeline 接入日志平台
|
* 生产环境走 JSON pipeline 接入日志平台
|
||||||
* 3. **不**重新抛出 —— 调用方拿到的永远是 `Result<T>`(throw 风格调用方可在
|
* 3. 不重新抛出 —— 调用方拿到的永远是 `Result<T>`(throw 风格调用方可在
|
||||||
* 最外层显式 `throw Result.unwrap(r)`)
|
* 最外层显式 `throw Result.unwrap(r)`)
|
||||||
*/
|
*/
|
||||||
async wrap<T>(thunk: () => Promise<T>): Promise<Result<T>> {
|
async wrap<T>(thunk: () => Promise<T>): Promise<Result<T>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user