diff --git a/public/images copy/3x/ic_user_vip.png b/public/images copy/3x/ic_user_vip.png deleted file mode 100644 index 6e1f4ee5..00000000 Binary files a/public/images copy/3x/ic_user_vip.png and /dev/null differ diff --git a/public/images copy/3x/ic_user_voicemessage.png b/public/images copy/3x/ic_user_voicemessage.png deleted file mode 100644 index 0b2964cc..00000000 Binary files a/public/images copy/3x/ic_user_voicemessage.png and /dev/null differ diff --git a/public/images copy/3x/ic_voicemessage_act.png b/public/images copy/3x/ic_voicemessage_act.png deleted file mode 100644 index 5b84de6a..00000000 Binary files a/public/images copy/3x/ic_voicemessage_act.png and /dev/null differ diff --git a/public/images copy/3x/ic_voicemessage_lock.png b/public/images copy/3x/ic_voicemessage_lock.png deleted file mode 100644 index 952bfb02..00000000 Binary files a/public/images copy/3x/ic_voicemessage_lock.png and /dev/null differ diff --git a/public/images copy/3x/pic_bg_vip.png b/public/images copy/3x/pic_bg_vip.png deleted file mode 100644 index 3c1788e1..00000000 Binary files a/public/images copy/3x/pic_bg_vip.png and /dev/null differ diff --git a/public/images copy/3x/pic_popup_voicemessage.png b/public/images copy/3x/pic_popup_voicemessage.png deleted file mode 100644 index 5b871715..00000000 Binary files a/public/images copy/3x/pic_popup_voicemessage.png and /dev/null differ diff --git a/public/images copy/ic_user_vip.png b/public/images copy/ic_user_vip.png deleted file mode 100644 index 12abecb9..00000000 Binary files a/public/images copy/ic_user_vip.png and /dev/null differ diff --git a/public/images copy/ic_user_voicemessage.png b/public/images copy/ic_user_voicemessage.png deleted file mode 100644 index afc0dd30..00000000 Binary files a/public/images copy/ic_user_voicemessage.png and /dev/null differ diff --git a/public/images copy/ic_voicemessage_act.png b/public/images copy/ic_voicemessage_act.png deleted file mode 100644 index 533d8524..00000000 Binary files a/public/images copy/ic_voicemessage_act.png and /dev/null differ diff --git a/public/images copy/ic_voicemessage_lock.png b/public/images copy/ic_voicemessage_lock.png deleted file mode 100644 index c37f2ddd..00000000 Binary files a/public/images copy/ic_voicemessage_lock.png and /dev/null differ diff --git a/public/images copy/pic_bg_vip.png b/public/images copy/pic_bg_vip.png deleted file mode 100644 index 7b928308..00000000 Binary files a/public/images copy/pic_bg_vip.png and /dev/null differ diff --git a/public/images copy/pic_popup_voicemessage.png b/public/images copy/pic_popup_voicemessage.png deleted file mode 100644 index 4cc26ad7..00000000 Binary files a/public/images copy/pic_popup_voicemessage.png and /dev/null differ diff --git a/public/images copy/2x/ic_user_vip.png b/public/images/ic_user_vip.png similarity index 100% rename from public/images copy/2x/ic_user_vip.png rename to public/images/ic_user_vip.png diff --git a/public/images copy/2x/ic_user_voicemessage.png b/public/images/ic_user_voicemessage.png similarity index 100% rename from public/images copy/2x/ic_user_voicemessage.png rename to public/images/ic_user_voicemessage.png diff --git a/public/images copy/2x/ic_voicemessage_act.png b/public/images/ic_voicemessage_act.png similarity index 100% rename from public/images copy/2x/ic_voicemessage_act.png rename to public/images/ic_voicemessage_act.png diff --git a/public/images copy/2x/ic_voicemessage_lock.png b/public/images/ic_voicemessage_lock.png similarity index 100% rename from public/images copy/2x/ic_voicemessage_lock.png rename to public/images/ic_voicemessage_lock.png diff --git a/public/images copy/2x/pic_bg_vip.png b/public/images/pic_bg_vip.png similarity index 100% rename from public/images copy/2x/pic_bg_vip.png rename to public/images/pic_bg_vip.png diff --git a/public/images copy/2x/pic_popup_voicemessage.png b/public/images/pic_popup_voicemessage.png similarity index 100% rename from public/images copy/2x/pic_popup_voicemessage.png rename to public/images/pic_popup_voicemessage.png diff --git a/public/manifest.json b/public/manifest.json index b3eb3b59..096969ce 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "name": "cozsweet", - "short_name": "cozsweet", + "name": "CozSweet", + "short_name": "CozSweet", "description": "Cozsweet — Your exclusive AI companion", "start_url": "/splash", "scope": "/", diff --git a/scripts/deploy/kill_next_ports.sh b/scripts/deploy/kill_next_ports.sh new file mode 100755 index 00000000..1006ec80 --- /dev/null +++ b/scripts/deploy/kill_next_ports.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Kill processes listening on the Next.js test/main deployment ports. + +PORTS="9135 9185" + +find_pids_by_port() { + port="$1" + if command -v lsof >/dev/null 2>&1; then + lsof -ti tcp:"$port" 2>/dev/null || true + return + fi + + if command -v fuser >/dev/null 2>&1; then + fuser "$port"/tcp 2>/dev/null || true + return + fi + + echo "WARN: neither lsof nor fuser is available; cannot inspect port $port" >&2 +} + +kill_port() { + port="$1" + pids="$(find_pids_by_port "$port")" + + if [ -z "$pids" ]; then + echo "port $port: no process found" + return + fi + + echo "port $port: stopping PID(s) $pids" + kill $pids 2>/dev/null || true + sleep 2 + + still_running="" + for pid in $pids; do + if kill -0 "$pid" 2>/dev/null; then + still_running="$still_running $pid" + fi + done + + if [ -n "$still_running" ]; then + echo "port $port: force killing PID(s)$still_running" + kill -9 $still_running 2>/dev/null || true + fi +} + +for port in $PORTS; do + kill_port "$port" +done diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index 6e235053..f592f12a 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -19,12 +19,14 @@ */ import { useEffect, useRef } from "react"; import Image from "next/image"; +import { useRouter } from "next/navigation"; import { useAuthState } from "@/stores/auth/auth-context"; import type { LoginStatus } from "@/data/dto/auth"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; import { AppEnvUtil } from "@/utils/app-env"; +import { ROUTES } from "@/router/routes"; import { MobileShell } from "@/app/_components/core"; @@ -75,6 +77,7 @@ function dispatchAuthLifecycle( } export function ChatScreen() { + const router = useRouter(); const state = useChatState(); const authState = useAuthState(); const chatDispatch = useChatDispatch(); @@ -91,37 +94,36 @@ export function ChatScreen() { const showExhaustedBanner = isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0; - - // ───────────────────────────────────────────────────────────── - // 屏幕生命周期 + 派初次鉴权生命周期事件 - // ───────────────────────────────────────────────────────────── - useEffect(() => { - dispatchAuthLifecycle(authState.loginStatus, chatDispatch); - return () => { - }; - // 注:mount useEffect 故意不依赖 loginStatus —— 首次 mount 时机是固定的 - // ("用户进 /chat 那一刻"),loginStatus 由 splash/auth-screen 提前 settle。 - // loginStatus 后续变化由下方"副作用 ②" 统一处理。 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chatDispatch]); - - // ───────────────────────────────────────────────────────────── - // 副作用 ②:loginStatus 变化 → 派对应鉴权生命周期事件 + // loginStatus 变化 → 派对应鉴权生命周期事件 // // 规则: - // - 首次 mount(prev === null)→ 由 mount useEffect 统一处理 - // - "notLoggedIn" → 派 ChatLogout + // - AuthInit 未完成 → 等,避免初始 notLoggedIn 把 /chat 变成死状态 + // - AuthInit 完成后仍 "notLoggedIn" → 清 chat 并回 splash // - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin // ───────────────────────────────────────────────────────────── const prevLoginStatusRef = useRef(null); useEffect(() => { + if (!authState.hasInitialized || authState.isLoading) return; + const prev = prevLoginStatusRef.current; - prevLoginStatusRef.current = authState.loginStatus; - if (prev === null) return; // 首次 mount 由 mount useEffect 处理 if (prev === authState.loginStatus) return; // 登录态没变 + prevLoginStatusRef.current = authState.loginStatus; + + if (authState.loginStatus === "notLoggedIn") { + chatDispatch({ type: "ChatLogout" }); + router.replace(ROUTES.splash); + return; + } + dispatchAuthLifecycle(authState.loginStatus, chatDispatch); - }, [authState.loginStatus, chatDispatch]); + }, [ + authState.hasInitialized, + authState.isLoading, + authState.loginStatus, + chatDispatch, + router, + ]); return ( diff --git a/src/stores/auth/auth-context.tsx b/src/stores/auth/auth-context.tsx index 89128ec7..88b85f5f 100644 --- a/src/stores/auth/auth-context.tsx +++ b/src/stores/auth/auth-context.tsx @@ -34,6 +34,8 @@ interface AuthState { errorMessage: string | null; /** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */ loginStatus: MachineContext["loginStatus"]; + /** 启动时的本地登录态恢复是否已经完成 */ + hasInitialized: MachineContext["hasInitialized"]; /** * 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮派完业务事件后置 true, * splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` @@ -67,12 +69,14 @@ export function AuthProvider({ children }: AuthProviderProps) { state.matches("loadingEmailLogin") || state.matches("loadingEmailRegister") || state.matches("loadingOAuth") || + state.matches("initializing") || state.matches("loggingOut") || state.matches("syncingGoogleBackend") || state.matches("syncingFacebookBackend") || state.matches("loadingGuestLogin"), errorMessage: state.context.errorMessage, loginStatus: state.context.loginStatus, + hasInitialized: state.context.hasInitialized, pendingRedirect: state.context.pendingRedirect, }), [state], diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index 9501502b..147d9df1 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -111,6 +111,7 @@ export const authMachine = setup({ target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地) actions: assign({ loginStatus: ({ event }) => event.output, + hasInitialized: true, // 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图" errorMessage: null, }), @@ -120,6 +121,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -137,6 +139,7 @@ export const authMachine = setup({ // Skip 点击后已在 splash 里立即跳 /chat;guest login 不再依赖 pendingRedirect。 pendingRedirect: false, errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -144,6 +147,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -164,6 +168,7 @@ export const authMachine = setup({ loginStatus: ({ event }) => event.output, pendingRedirect: true, // ← 邮箱登录成功 → 跳 errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -171,6 +176,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -196,6 +202,7 @@ export const authMachine = setup({ loginStatus: ({ event }) => event.output, pendingRedirect: true, // ← 注册成功 → 跳 errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -203,6 +210,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -236,12 +244,16 @@ export const authMachine = setup({ src: "logout", onDone: { target: "idle", - actions: assign(() => initialState), + actions: assign(() => ({ + ...initialState, + hasInitialized: true, + })), }, onError: { target: "idle", actions: assign(({ event }) => ({ ...initialState, + hasInitialized: true, errorMessage: event.error instanceof Error ? event.error.message : String(event.error), })), @@ -263,6 +275,7 @@ export const authMachine = setup({ loginStatus: ({ event }) => event.output, pendingRedirect: true, // ← OAuth sync 成功 → 跳 errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -270,6 +283,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -289,6 +303,7 @@ export const authMachine = setup({ loginStatus: ({ event }) => event.output, pendingRedirect: true, // ← OAuth sync 成功 → 跳 errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -296,6 +311,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, diff --git a/src/stores/auth/auth-state.ts b/src/stores/auth/auth-state.ts index 80eb8471..84397bc1 100644 --- a/src/stores/auth/auth-state.ts +++ b/src/stores/auth/auth-state.ts @@ -15,6 +15,8 @@ export interface AuthState { errorMessage: string | null; /** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */ loginStatus: LoginStatus; + /** 启动时的本地登录态恢复是否已经完成 */ + hasInitialized: boolean; /** * 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮刚派事件 → 置 true * → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转 @@ -35,5 +37,6 @@ export const initialState: AuthState = { confirmPassword: "", errorMessage: null, loginStatus: "notLoggedIn", + hasInitialized: false, pendingRedirect: false, };