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