From 9ffa30cc03f62ef5c1c687a05e58cb1084cbc3d9 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 16 Jun 2026 14:06:31 +0800 Subject: [PATCH 1/3] 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. --- src/app/auth/components/auth-screen.tsx | 4 +- src/app/chat/components/chat-header.tsx | 2 +- src/app/chat/components/chat-screen.tsx | 28 +++--- src/app/chat/components/message-content.tsx | 2 +- .../deviceid/[deviceId]/DeepLinkPersist.tsx | 2 +- src/app/chat/deviceid/[deviceId]/page.tsx | 2 +- src/app/page.tsx | 2 +- src/app/splash/components/splash-button.tsx | 10 +-- src/core/fonts.ts | 4 +- src/core/net/interceptor/token_interceptor.ts | 2 +- src/data/repositories/auth_repository.ts | 8 +- .../interfaces/iauth_repository.ts | 2 +- src/data/schemas/chat/chat_send_response.ts | 2 +- src/data/storage/chat/chat_storage.ts | 6 +- src/lib/auth/auth.ts | 2 +- src/lib/auth/auth_platform.ts | 4 +- src/lib/auth/logout.ts | 2 +- src/providers/session-provider.tsx | 2 +- src/router/index.ts | 2 +- src/router/proxy.ts | 16 ++-- src/router/routes.ts | 4 +- src/stores/auth/auth-actors.ts | 12 +-- src/stores/auth/auth-events.ts | 10 +-- src/stores/auth/auth-machine.ts | 16 ++-- src/stores/auth/auth-state.ts | 6 +- src/stores/auth/oauth-session-sync.tsx | 2 +- src/stores/chat/chat-context.tsx | 10 +-- src/stores/chat/chat-events.ts | 18 ++-- src/stores/chat/chat-machine.actors.ts | 46 +++++----- src/stores/chat/chat-machine.helpers.ts | 62 ++++++------- src/stores/chat/chat-machine.ts | 86 +++++++++---------- src/stores/chat/chat-state.ts | 32 +++---- src/utils/result.ts | 4 +- 33 files changed, 206 insertions(+), 206 deletions(-) diff --git a/src/app/auth/components/auth-screen.tsx b/src/app/auth/components/auth-screen.tsx index 9828a01b..7ae6cdaf 100644 --- a/src/app/auth/components/auth-screen.tsx +++ b/src/app/auth/components/auth-screen.tsx @@ -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, diff --git a/src/app/chat/components/chat-header.tsx b/src/app/chat/components/chat-header.tsx index 6bf693bf..7e0bdcdb 100644 --- a/src/app/chat/components/chat-header.tsx +++ b/src/app/chat/components/chat-header.tsx @@ -8,7 +8,7 @@ * - 游客模式(isGuest=true):渲染 `GuestBanner` 提示注册(点击 → /auth) * - 登录模式(isGuest=false):渲染 `MenuButton`(点击 → push 到 /sidebar) * - * 注意:菜单按钮直接 push 到侧边栏路由,**不**打开下拉菜单 + * 注意:菜单按钮直接 push 到侧边栏路由,不打开下拉菜单 * (Dart 端 `MenuButton(onTap: _goSidebar)` 行为)。 */ import { useRouter } from "next/navigation"; diff --git a/src/app/chat/components/chat-screen.tsx b/src/app/chat/components/chat-screen.tsx index 346e8ee7..66a3899f 100644 --- a/src/app/chat/components/chat-screen.tsx +++ b/src/app/chat/components/chat-screen.tsx @@ -5,9 +5,9 @@ * 原始 Dart: lib/ui/chat/chat_screen.dart(172 行) * * 职责(本文件): - * - **订阅 auth 状态机**(loginStatus)—— 派生 isGuest / **派鉴权生命周期事件**给 chat 机器 + * - 订阅 auth 状态机(loginStatus)—— 派生 isGuest / 派鉴权生命周期事件给 chat 机器 * - 屏幕生命周期事件(init / visible / invisible) - * - **不**直接管理 WebSocket / **不**直接读 AuthStorage(除 token 取值) + * - 不直接管理 WebSocket / 不直接读 AuthStorage(除 token 取值) * * 子组件职责: * - ChatHeader:顶部栏(游客 banner / 菜单按钮) @@ -16,13 +16,13 @@ * - PwaInstallOverlay:PWA 安装提示触发器 * - 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, ): void { if (loginStatus === "guest") { - // 游客:派 ChatGuestLogin(chat 机器**不**连 WS —— 业务事实"断 WS") + // 游客:派 ChatGuestLogin(chat 机器不连 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]); diff --git a/src/app/chat/components/message-content.tsx b/src/app/chat/components/message-content.tsx index dea5e557..e55671b6 100644 --- a/src/app/chat/components/message-content.tsx +++ b/src/app/chat/components/message-content.tsx @@ -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, diff --git a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx index c753a45d..a8157fd1 100644 --- a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx +++ b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx @@ -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, ...)`, diff --git a/src/app/chat/deviceid/[deviceId]/page.tsx b/src/app/chat/deviceid/[deviceId]/page.tsx index 31de2bb0..687530a1 100644 --- a/src/app/chat/deviceid/[deviceId]/page.tsx +++ b/src/app/chat/deviceid/[deviceId]/page.tsx @@ -9,7 +9,7 @@ * Next.js 16 全局 helper `PageProps<'/literal'>` 由 `next dev` / `next build` / * `next typegen` 生成。 * - * 这里**不**调用 `redirect()`:把 fbid 暴露在 URL 上对用户不友好; + * 这里不调用 `redirect()`:把 fbid 暴露在 URL 上对用户不友好; * 跳转推迟到 Client 完成持久化之后。 */ diff --git a/src/app/page.tsx b/src/app/page.tsx index 8b5815ec..3b935c5e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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。 */ diff --git a/src/app/splash/components/splash-button.tsx b/src/app/splash/components/splash-button.tsx index ab82cac4..6e4c81c9 100644 --- a/src/app/splash/components/splash-button.tsx +++ b/src/app/splash/components/splash-button.tsx @@ -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 (
- {/* Skip 按钮 = 显式游客登录入口(派发 AuthGuestLoginSubmitted,**不**做路由跳转) */} + {/* Skip 按钮 = 显式游客登录入口(派发 AuthGuestLoginSubmitted,不做路由跳转) */} +
+ ); +} diff --git a/src/app/chat/components/chat-screen.tsx b/src/app/chat/components/chat-screen.tsx index 66a3899f..c2103c1a 100644 --- a/src/app/chat/components/chat-screen.tsx +++ b/src/app/chat/components/chat-screen.tsx @@ -2,8 +2,6 @@ /** * ChatScreen 编排层 * - * 原始 Dart: lib/ui/chat/chat_screen.dart(172 行) - * * 职责(本文件): * - 订阅 auth 状态机(loginStatus)—— 派生 isGuest / 派鉴权生命周期事件给 chat 机器 * - 屏幕生命周期事件(init / visible / invisible) @@ -38,9 +36,11 @@ import { BrowserHintOverlay } from "./browser-hint-overlay"; import { ChatArea } from "./chat-area"; import { ChatHeader } from "./chat-header"; import { ChatInputBar } from "./chat-input-bar"; +import { ChatQuotaExhaustedBanner } from "./chat-quota-exhausted-banner"; import { PwaInstallOverlay } from "./pwa-install-overlay"; import styles from "./chat-screen.module.css"; + /** * 派生 isGuest —— 单一来源:`auth.loginStatus` */ @@ -84,6 +84,17 @@ export function ChatScreen() { // isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段) const isGuest = deriveIsGuest(authState.loginStatus); + // 游客配额耗尽 → 显示 Unlock CTA banner(替代 ChatInputBar) + // 三重条件:游客 + 配额加载完 + 用户被配额 guard 拦截过至少一次 + // - quotaLoaded 避免一进 /chat 就跳 banner(loadQuota 还在 in-flight) + // - quotaExceededTrigger 而非 guestTotalQuota / guestRemainingQuota —— + // trigger 由 chat 机器的 guard 在 ChatSendMessage / ChatSendImage 被拦截时 +1, + // 是 "用户被配额拒绝过" 的权威信号(避免依赖 quota 数值本身的边界判断) + const showExhaustedBanner = + isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0; + + + // ───────────────────────────────────────────────────────────── // 屏幕生命周期 + 派初次鉴权生命周期事件 // ───────────────────────────────────────────────────────────── @@ -138,7 +149,12 @@ export function ChatScreen() { isGuest={isGuest} /> - + {showExhaustedBanner ? ( + + ) : ( + + )} + {/* 浏览器提示(应用内浏览器检测) */} diff --git a/src/app/chat/components/index.ts b/src/app/chat/components/index.ts index 8b298016..5a830fd3 100644 --- a/src/app/chat/components/index.ts +++ b/src/app/chat/components/index.ts @@ -8,7 +8,9 @@ export * from "./chat-area"; export * from "./chat-header"; export * from "./chat-input-bar"; export * from "./chat-input-text-field"; +export * from "./chat-quota-exhausted-banner"; export * from "./chat-screen"; + export * from "./chat-send-button"; export * from "./date-header"; export * from "./fullscreen-image-viewer"; diff --git a/src/data/storage/chat/chat_storage.ts b/src/data/storage/chat/chat_storage.ts index ced0799a..cdffd6cf 100644 --- a/src/data/storage/chat/chat_storage.ts +++ b/src/data/storage/chat/chat_storage.ts @@ -1,19 +1,5 @@ "use client"; -/** - * ChatStorage 完整实现(基于 unstorage 抽象) - * - * 对齐 Dart 端 `ChatStorage`(lib/data/services/storage/chat/chat_storage.dart): - * - 游客每日聊天配额(GuestChatQuota:{ remaining, date }) - * - 游客总配额(int) - * - * 注意:本类与 `LocalChatStorage`(IndexedDB / Dexie)是不同类: - * - ChatStorage = 游客配额等元数据(KV) - * - LocalChatStorage = 聊天记录本身(IndexedDB) - * - * 设计:内部使用 SpAsyncUtil 静态类 + Zod 校验;保持对外 API 完全兼容。 - */ - import { GuestChatQuotaSchema, type GuestChatQuotaData, diff --git a/src/stores/chat/chat-context.tsx b/src/stores/chat/chat-context.tsx index 896a6349..d286952c 100644 --- a/src/stores/chat/chat-context.tsx +++ b/src/stores/chat/chat-context.tsx @@ -1,16 +1,5 @@ "use client"; -/** - * ChatContext:基于 XState v5 的 React Context Provider - * - * 业务事件通过 `useMachine(chatMachine).send()` 派发。 - * XState 自动处理 HTTP 副作用(invoke + fromPromise actors)。 - * - * 兼容性:保持原 `useChatState()` / `useChatDispatch()` API。 - * 内部将 XState 状态机快照映射回原 `ChatState` 形状。 - * - * 注:WebSocket 长生命周期 actor 由 chat-screen.tsx 监听 auth 状态机驱动—— - * 鉴权解耦(loginStatus 在 auth,chat 机器不感知鉴权)。 - */ + import { type Dispatch, type ReactNode, diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index 92b368ba..e56d66a5 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -33,7 +33,8 @@ import { setup, assign } from "xstate"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; -import { formatDate } from "@/utils/date"; +import { formatDate, todayString } from "@/utils/date"; + import { ChatState, initialState } from "./chat-state"; import type { ChatEvent } from "./chat-events"; @@ -85,8 +86,9 @@ export const chatMachine = setup({ // 持久化两个额度( fire-and-forget,不 await —— assign 是 sync) // daily quota 需要 today 字符串(让 ChatStorage 跨天判断 reset) - const today = formatDate(); + const today = todayString(); ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today); + ChatStorage.getInstance().setGuestTotalQuota(newTotal); console.log("[chat-machine] appendUserMessage", { @@ -124,9 +126,12 @@ export const chatMachine = setup({ const newTotal = context.wsConnected ? context.guestTotalQuota : Math.max(0, context.guestTotalQuota - 1); - const today = formatDate(); + + const today = todayString(); void ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today); + void ChatStorage.getInstance().setGuestTotalQuota(newTotal); + console.log("[chat-machine] appendUserImage", { oldMessagesCount: context.messages.length, wsConnected: context.wsConnected, diff --git a/src/stores/chat/chat-state.ts b/src/stores/chat/chat-state.ts index 02ec6607..85dabdae 100644 --- a/src/stores/chat/chat-state.ts +++ b/src/stores/chat/chat-state.ts @@ -3,13 +3,6 @@ * * 注:GuestChatQuota 仍位于 chat-machine.ts(属于业务常量,与上下文配对紧密)。 * - * 历史: - * - `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 同时表示"未初始化"和"已耗尽" —— UI 用 `if (isGuest && ...)` 天然过滤 - * * 新增 `wsConnected: boolean`(仅 userSession 期间 true): * - WS 已连 → 消息发送走 WS,不消耗次数 * - WS 未连 → 消息发送走 HTTP,消耗次数 From 02b1b713a5baf6da9290a13defec676b47a8b6d4 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 16 Jun 2026 15:30:23 +0800 Subject: [PATCH 3/3] style(splash): set Athelas as primary font and italicize button text - Prioritize Athelas in --font-system font stack with system fonts as fallback - Apply italic style to splash button heading and label for typography emphasis --- src/app/splash/components/splash-button.module.css | 2 ++ src/tokens/typography.css | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/splash/components/splash-button.module.css b/src/app/splash/components/splash-button.module.css index 34bce9af..a1b4ca30 100644 --- a/src/app/splash/components/splash-button.module.css +++ b/src/app/splash/components/splash-button.module.css @@ -12,6 +12,7 @@ font-family: var(--font-athelas); font-size: var(--font-size-xxl); font-weight: 700; + font-style: italic; color: #ffffff; background: transparent; border: 0; @@ -43,6 +44,7 @@ color: #ffffff; font-weight: 700; cursor: pointer; + font-style: italic; display: inline-flex; align-items: center; justify-content: center; diff --git a/src/tokens/typography.css b/src/tokens/typography.css index 320c436f..40352ca3 100644 --- a/src/tokens/typography.css +++ b/src/tokens/typography.css @@ -7,8 +7,12 @@ */ @theme { /* 字体族 */ - --font-system: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", - Roboto, "Helvetica Neue", Arial, sans-serif; + /* + * --font-system 优先用 Athelas(Apple 平台专用字体 — next/font/local,所有平台都能加载)。 + * 不支持的字符自动回退到 system-ui 等系统字体(浏览器 font fallback 机制)。 + */ + --font-system: var(--font-athelas), system-ui, -apple-system, + BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-athelas: "Athelas", serif; /* Tailwind 默认字体别名:font-sans → font-system */