From b67beaeae6af9e266957171a514ad8edfcb26a6b Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 15:13:44 +0800 Subject: [PATCH 1/7] refactor(auth): remove dead AuthReset event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AuthReset was declared in the AuthEvent union and handled in auth-machine.idle.on with `assign(() => initialState)`, but nothing in the codebase actually dispatches it anymore. History of dispatch sites (now all gone): - sidebar-screen.tsx previously dispatched AuthReset post-logout alongside router.replace(ROUTES.chat) — that was simplified to just dispatch AuthLogoutSubmitted, the state machine's loggingOut state now does the context reset itself in onDone via the same `assign(() => initialState)` action. - chat-screen / splash / auth screens never dispatched it. - The /auth screen's pendingRedirect-based redirect loop doesn't need it (the redirect is one-shot, not a state that needs resetting). So the event handler is unreachable. Remove the type member from auth-events.ts and the handler from auth-machine.ts. No behavior change: the only path that 'reset auth state' was already covered by loggingOut.onDone. Verified via `grep -rn 'AuthReset' src/`: only 2 hits remain after this commit — both are the type declaration and the machine handler, which is now also gone (zero hits). --- src/stores/auth/auth-events.ts | 1 - src/stores/auth/auth-machine.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/stores/auth/auth-events.ts b/src/stores/auth/auth-events.ts index e01d58b9..c58b5b6a 100644 --- a/src/stores/auth/auth-events.ts +++ b/src/stores/auth/auth-events.ts @@ -12,7 +12,6 @@ export type AuthEvent = | { type: "AuthPanelModeChanged"; mode: AuthPanelMode } | { type: "AuthModeChanged"; mode: AuthMode } | { type: "AuthFormCleared" } - | { type: "AuthReset" } | { type: "AuthLogoutSubmitted" } /** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */ | { type: "AuthInit" } diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index 56efeb4d..9501502b 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -78,9 +78,6 @@ export const authMachine = setup({ errorMessage: null, }), }, - AuthReset: { - actions: assign(() => initialState), - }, AuthLogoutSubmitted: "loggingOut", // 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 派发 AuthInit: "initializing", From 977c0905bd3dc7475a1faecbe8f300ed9d1bd360 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 15:30:36 +0800 Subject: [PATCH 2/7] refactor(chat): remove redundant auth state management from ChatScreen documentation refactor(chat): update guest chat quota logic to use isProduction utility fix(user): add TODO for user initialization event on login status change --- src/app/chat/chat-screen.tsx | 5 ----- src/data/dto/chat/guest_chat_quota.ts | 22 ++++++++-------------- src/stores/user/user-auth-sync.tsx | 1 + 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index eb3f0d8f..a247f6db 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -2,11 +2,6 @@ /** * ChatScreen 编排层 * - * 职责(本文件): - * - 订阅 auth 状态机(loginStatus)—— 派生 isGuest / 派鉴权生命周期事件给 chat 机器 - * - 屏幕生命周期事件(init / visible / invisible) - * - 不直接管理 WebSocket / 不直接读 AuthStorage(除 token 取值) - * * 子组件职责: * - ChatHeader:顶部栏(游客 banner / 菜单按钮) * - ChatArea:消息列表 diff --git a/src/data/dto/chat/guest_chat_quota.ts b/src/data/dto/chat/guest_chat_quota.ts index 8bd7204d..eccc2c86 100644 --- a/src/data/dto/chat/guest_chat_quota.ts +++ b/src/data/dto/chat/guest_chat_quota.ts @@ -6,6 +6,7 @@ import { type GuestChatQuotaInput, type GuestChatQuotaData, } from "@/data/schemas/chat/guest_chat_quota"; +import { isProduction } from "@/utils/app-env"; export class GuestChatQuota { // 静态常量 @@ -13,7 +14,7 @@ export class GuestChatQuota { static readonly maxQuotaPerDayTest = 4; static readonly defaultTotalQuota = 50; - static readonly defaultTotalQuotaDev = 5; + static readonly defaultTotalQuotaTest = 5; static readonly quotaExhausted = 0; declare readonly remaining: number; @@ -40,29 +41,22 @@ export class GuestChatQuota { return GuestChatQuota.quotaExhausted; } - /** - * 是否处于开发环境 - */ - static get isDevelopment(): boolean { - return process.env.NODE_ENV !== "production"; - } - /** * 获取当前环境的每日最大配额 */ static get threshold(): number { - return GuestChatQuota.isDevelopment - ? GuestChatQuota.maxQuotaPerDayTest - : GuestChatQuota.maxQuotaPerDay; + return isProduction() + ? GuestChatQuota.maxQuotaPerDay + : GuestChatQuota.maxQuotaPerDayTest; } /** * 获取当前环境的总配额默认值 */ static get totalQuotaDefault(): number { - return GuestChatQuota.isDevelopment - ? GuestChatQuota.defaultTotalQuotaDev - : GuestChatQuota.defaultTotalQuota; + return isProduction() + ? GuestChatQuota.defaultTotalQuota + : GuestChatQuota.defaultTotalQuotaTest; } /** diff --git a/src/stores/user/user-auth-sync.tsx b/src/stores/user/user-auth-sync.tsx index 26911c9d..78710f42 100644 --- a/src/stores/user/user-auth-sync.tsx +++ b/src/stores/user/user-auth-sync.tsx @@ -19,6 +19,7 @@ export function UserAuthSync() { const userDispatch = useUserDispatch(); const lastInitializedStatusRef = useRef(null); + //TODO 在测位栏界面监听变化,监听登录状态的变化若发生变化,则需要分发 user init 事件 useEffect(() => { if (loginStatus === "notLoggedIn" || loginStatus === "guest") { lastInitializedStatusRef.current = null; From a6ee0d5e24bf0731dbd1f9c0dedf369511ed1788 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 15:31:15 +0800 Subject: [PATCH 3/7] refactor(env): centralize app environment detection --- src/core/net/config/api_config.ts | 7 +++--- src/data/dto/chat/guest_chat_quota.ts | 6 ++--- src/utils/app-env.ts | 35 +++++++++++++++++++++------ src/utils/logger.ts | 12 ++++----- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/core/net/config/api_config.ts b/src/core/net/config/api_config.ts index dd1db524..c3ec69a7 100644 --- a/src/core/net/config/api_config.ts +++ b/src/core/net/config/api_config.ts @@ -4,8 +4,9 @@ * 通过环境变量配置后端服务地址与超时时间。 * 原始 Dart: lib/core/net/service_manager.dart */ +import { AppEnvUtil, type AppEnv } from "@/utils/app-env"; -export type AppEnv = "development" | "test" | "production"; +export type { AppEnv }; export interface ApiConfig { /** 后端 API 基础 URL(含协议) */ @@ -33,9 +34,7 @@ export class TimeoutConstants { * 解析当前环境 */ export function getAppEnv(): AppEnv { - const env = process.env.NEXT_PUBLIC_APP_ENV; - if (env === "production" || env === "test") return env; - return "development"; + return AppEnvUtil.current; } /** diff --git a/src/data/dto/chat/guest_chat_quota.ts b/src/data/dto/chat/guest_chat_quota.ts index eccc2c86..19e63efd 100644 --- a/src/data/dto/chat/guest_chat_quota.ts +++ b/src/data/dto/chat/guest_chat_quota.ts @@ -6,7 +6,7 @@ import { type GuestChatQuotaInput, type GuestChatQuotaData, } from "@/data/schemas/chat/guest_chat_quota"; -import { isProduction } from "@/utils/app-env"; +import { AppEnvUtil } from "@/utils/app-env"; export class GuestChatQuota { // 静态常量 @@ -45,7 +45,7 @@ export class GuestChatQuota { * 获取当前环境的每日最大配额 */ static get threshold(): number { - return isProduction() + return AppEnvUtil.isProduction() ? GuestChatQuota.maxQuotaPerDay : GuestChatQuota.maxQuotaPerDayTest; } @@ -54,7 +54,7 @@ export class GuestChatQuota { * 获取当前环境的总配额默认值 */ static get totalQuotaDefault(): number { - return isProduction() + return AppEnvUtil.isProduction() ? GuestChatQuota.defaultTotalQuota : GuestChatQuota.defaultTotalQuotaTest; } diff --git a/src/utils/app-env.ts b/src/utils/app-env.ts index 96978737..1e7e5b74 100644 --- a/src/utils/app-env.ts +++ b/src/utils/app-env.ts @@ -3,13 +3,34 @@ * * 原始 Dart: lib/utils/app_env_util.dart * - * 替代 `AppEnvUtil.isDevelopment`:基于 Next.js 注入的 `process.env.NODE_ENV`。 + * 基于 `.env*` 中的 `NEXT_PUBLIC_APP_ENV` 判断当前业务环境。 + * + * 注意:该变量带 `NEXT_PUBLIC_` 前缀,可同时用于服务端和客户端代码。 */ -export const isDevelopment = (): boolean => - process.env.NODE_ENV !== "production"; +export type AppEnv = "development" | "test" | "production"; -export const isProduction = (): boolean => - process.env.NODE_ENV === "production"; +export class AppEnvUtil { + private constructor() {} -export const isTest = (): boolean => - process.env.NODE_ENV === "test"; + static get current(): AppEnv { + const env = process.env.NEXT_PUBLIC_APP_ENV; + if (env === "production" || env === "test") return env; + return "development"; + } + + static isDevelopment(): boolean { + return AppEnvUtil.current === "development"; + } + + static isProduction(): boolean { + return AppEnvUtil.current === "production"; + } + + static isTest(): boolean { + return AppEnvUtil.current === "test"; + } + + static canOutputLogs(): boolean { + return !AppEnvUtil.isProduction(); + } +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 87ac2d7c..3cd858f0 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,22 +1,22 @@ import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino"; -import { isProduction, isTest } from "./app-env"; +import { AppEnvUtil } from "./app-env"; /** * 全局 pino root logger(单例,进程级复用)。 * - * 配置驱动(基于 `app-env.ts` 的 `isProduction()` / `isTest()`): - * - production: info 级别 + JSON(机器可解析,接入日志平台) + * 配置驱动(基于 `app-env.ts` 的 `AppEnvUtil`): + * - production: silent(不输出日志) * - test: warn 级别(减少 vitest 噪音) * - development: debug 级别 + ISO 时间戳(人眼友好) */ const rootLogger: PinoLogger = pino(createLoggerOptions()); function createLoggerOptions(): LoggerOptions { - if (isProduction()) { - return { level: "info" }; + if (!AppEnvUtil.canOutputLogs()) { + return { level: "silent" }; } - if (isTest()) { + if (AppEnvUtil.isTest()) { return { level: "warn" }; } // development From f7201fa65429b1f367ba4d014a312f69344a355a Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 15:54:05 +0800 Subject: [PATCH 4/7] fix(chat): make send button circular --- src/app/chat/components/chat-send-button.module.css | 5 +++-- src/app/chat/components/chat-send-button.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/chat/components/chat-send-button.module.css b/src/app/chat/components/chat-send-button.module.css index 0ceb4e4a..20a33e95 100644 --- a/src/app/chat/components/chat-send-button.module.css +++ b/src/app/chat/components/chat-send-button.module.css @@ -2,10 +2,11 @@ .button { flex: 0 0 auto; - width: 48px; + width: 40px; height: 40px; + aspect-ratio: 1 / 1; border: 0; - border-radius: 9999px; + border-radius: 50%; background: var(--color-button-gradient-end, #fc69df); color: #fff; cursor: pointer; diff --git a/src/app/chat/components/chat-send-button.tsx b/src/app/chat/components/chat-send-button.tsx index d2f3fe90..d643b720 100644 --- a/src/app/chat/components/chat-send-button.tsx +++ b/src/app/chat/components/chat-send-button.tsx @@ -8,9 +8,9 @@ * - 默认:粉色背景 + 渐变边缘 * - 聚焦 / 激活:完整 primaryGradient(accent → 透明) * - * 图标:lucide-react (tree-shakable,currentColor 继承父 color) + * 图标:lucide-react (tree-shakable,currentColor 继承父 color) */ -import { ArrowUp, Send } from "lucide-react"; +import { ArrowUp } from "lucide-react"; import styles from "./chat-send-button.module.css"; From 5c26ad9d5b481a602d8c38202c77a0b774a30ea7 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 16:05:20 +0800 Subject: [PATCH 5/7] refactor(splash): remove Athelas font references from CSS files --- src/app/splash/components/splash-button.module.css | 2 -- src/app/splash/components/splash-content.tsx | 7 ------- src/app/splash/components/splash-logo.module.css | 2 -- src/app/splash/components/splash-screen.module.css | 1 - 4 files changed, 12 deletions(-) diff --git a/src/app/splash/components/splash-button.module.css b/src/app/splash/components/splash-button.module.css index 6cece395..e24d7c37 100644 --- a/src/app/splash/components/splash-button.module.css +++ b/src/app/splash/components/splash-button.module.css @@ -9,7 +9,6 @@ } .skip { - font-family: var(--font-athelas); font-size: var(--font-size-xxl); font-weight: 700; font-style: italic; @@ -58,7 +57,6 @@ } .facebookLabel { - font-family: var(--font-athelas); font-size: var(--font-size-xxl); font-weight: 700; color: #ffffff; diff --git a/src/app/splash/components/splash-content.tsx b/src/app/splash/components/splash-content.tsx index fdef115d..7dea7435 100644 --- a/src/app/splash/components/splash-content.tsx +++ b/src/app/splash/components/splash-content.tsx @@ -1,12 +1,5 @@ /** * Splash 主内容文案 - * - * 原始 Dart: lib/ui/splash/widgets/splash_content.dart - * - * 关键差异: - * - 使用 Athelas 字体(衬线体) - * - 标题加粗 + 斜体(与原 Dart 一致) - * - 颜色:白色(背景图上需高对比度) */ import styles from "./splash-content.module.css"; diff --git a/src/app/splash/components/splash-logo.module.css b/src/app/splash/components/splash-logo.module.css index 8e9829dd..6abba44b 100644 --- a/src/app/splash/components/splash-logo.module.css +++ b/src/app/splash/components/splash-logo.module.css @@ -7,6 +7,4 @@ display: block; width: auto; height: auto; - /* Apple 平台 Athelas 渲染时仍可能需要 fallback */ - font-family: var(--font-athelas, serif); } diff --git a/src/app/splash/components/splash-screen.module.css b/src/app/splash/components/splash-screen.module.css index d7a611da..84d90537 100644 --- a/src/app/splash/components/splash-screen.module.css +++ b/src/app/splash/components/splash-screen.module.css @@ -41,7 +41,6 @@ .bottom { margin: var(--spacing-xxxl) 0 0 0; - font-family: var(--font-athelas); font-size: var(--font-size-md); font-weight: 400; line-height: 1.5; From 62468ffc6a4a1b5a14b53d5f26e0618d4cfb3930 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 16:07:10 +0800 Subject: [PATCH 6/7] fix(theme): use next font athelas variable globally --- src/core/fonts.ts | 4 ++-- src/tokens/typography.css | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/fonts.ts b/src/core/fonts.ts index 0807400b..0cdc0a53 100644 --- a/src/core/fonts.ts +++ b/src/core/fonts.ts @@ -9,7 +9,7 @@ import localFont from "next/font/local"; * - 产出的 CSS 变量由 `globals.css` 桥接到 Tailwind(如 `font-sans`、`font-athelas`)。 * * 注意:本文件不通过 `src/lib/index.ts` 桶导出,避免被误打进客户端 bundle。 - * 引用方式:`import { geistSans, geistMono, athelas } from "@/lib/fonts";` + * 引用方式:`import { geistSans, geistMono, athelas } from "@/core/fonts";` */ const geistSans = Geist({ @@ -29,7 +29,7 @@ const geistMono = Geist_Mono({ * 放置位置:/public/fonts/Athelas-*.ttf * * 路径计算(`next/font/local` 要求相对路径从调用文件出发): - * src/lib/fonts.ts → ../../public/fonts/... + * src/core/fonts.ts → ../../public/fonts/... */ const athelas = localFont({ src: [ diff --git a/src/tokens/typography.css b/src/tokens/typography.css index 40352ca3..09459a8c 100644 --- a/src/tokens/typography.css +++ b/src/tokens/typography.css @@ -13,7 +13,6 @@ */ --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 */ --font-sans: var(--font-system); From f39e7e0672ad72f7663be58333e24b0176d6f980 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 16:19:21 +0800 Subject: [PATCH 7/7] fix(pwa): suppress install dialog in in-app browsers --- src/app/chat/components/pwa-install-overlay.tsx | 2 ++ src/integrations/index.ts | 1 - src/{integrations => utils}/browser-detect.ts | 9 --------- 3 files changed, 2 insertions(+), 10 deletions(-) rename src/{integrations => utils}/browser-detect.ts (79%) diff --git a/src/app/chat/components/pwa-install-overlay.tsx b/src/app/chat/components/pwa-install-overlay.tsx index dddfc007..fc9a71a2 100644 --- a/src/app/chat/components/pwa-install-overlay.tsx +++ b/src/app/chat/components/pwa-install-overlay.tsx @@ -14,6 +14,7 @@ */ import { useEffect } from "react"; +import { BrowserDetector } from "@/utils/browser-detect"; import { SpAsyncUtil } from "@/utils/storage"; import { pwaUtil } from "@/utils/pwa"; @@ -32,6 +33,7 @@ export function PwaInstallOverlay() { // isInstalled() 检查 standalone display-mode(避免给已安装用户再弹) if (!pwaUtil.isSupported()) return; if (pwaUtil.isInstalled()) return; + if (BrowserDetector.isInAppBrowser()) return; // 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。 pwaUtil.prepareInstallPrompt(); diff --git a/src/integrations/index.ts b/src/integrations/index.ts index 9652de67..30be615f 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -2,5 +2,4 @@ * @file Automatically generated by barrelsby. */ -export * from "./browser-detect"; export * from "./media-recorder"; diff --git a/src/integrations/browser-detect.ts b/src/utils/browser-detect.ts similarity index 79% rename from src/integrations/browser-detect.ts rename to src/utils/browser-detect.ts index fbcd1d5f..4f473193 100644 --- a/src/integrations/browser-detect.ts +++ b/src/utils/browser-detect.ts @@ -29,13 +29,4 @@ export const BrowserDetector = { return /WebView|; wv\)|FBAN|FBAV|MicroMessenger|Instagram|Line\//i.test(userAgent); }, - /** 当前是否以 PWA standalone 模式运行 */ - isPWAStandalone(): boolean { - if (typeof window === "undefined") return false; - return ( - window.matchMedia?.("(display-mode: standalone)").matches === true || - // iOS Safari 旧版 - (navigator as Navigator & { standalone?: boolean }).standalone === true - ); - }, };