Merge branch 'dev' into test

This commit is contained in:
2026-06-17 19:44:35 +08:00
24 changed files with 98 additions and 24 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "cozsweet", "name": "CozSweet",
"short_name": "cozsweet", "short_name": "CozSweet",
"description": "Cozsweet — Your exclusive AI companion", "description": "Cozsweet — Your exclusive AI companion",
"start_url": "/splash", "start_url": "/splash",
"scope": "/", "scope": "/",
+49
View File
@@ -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
+23 -21
View File
@@ -19,12 +19,14 @@
*/ */
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation";
import { useAuthState } from "@/stores/auth/auth-context"; import { useAuthState } from "@/stores/auth/auth-context";
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/dto/auth";
import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import { AppEnvUtil } from "@/utils/app-env"; import { AppEnvUtil } from "@/utils/app-env";
import { ROUTES } from "@/router/routes";
import { MobileShell } from "@/app/_components/core"; import { MobileShell } from "@/app/_components/core";
@@ -75,6 +77,7 @@ function dispatchAuthLifecycle(
} }
export function ChatScreen() { export function ChatScreen() {
const router = useRouter();
const state = useChatState(); const state = useChatState();
const authState = useAuthState(); const authState = useAuthState();
const chatDispatch = useChatDispatch(); const chatDispatch = useChatDispatch();
@@ -91,37 +94,36 @@ export function ChatScreen() {
const showExhaustedBanner = const showExhaustedBanner =
isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0; isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0;
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// 屏幕生命周期 + 派初次鉴权生命周期事件 // loginStatus 变化 → 派对应鉴权生命周期事件
// ─────────────────────────────────────────────────────────────
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 变化 → 派对应鉴权生命周期事件
// //
// 规则: // 规则:
// - 首次 mountprev === null)→ 由 mount useEffect 统一处理 // - AuthInit 未完成 → 等,避免初始 notLoggedIn 把 /chat 变成死状态
// - "notLoggedIn" → 派 ChatLogout // - AuthInit 完成后仍 "notLoggedIn" → 清 chat 并回 splash
// - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin // - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
const prevLoginStatusRef = useRef<LoginStatus | null>(null); const prevLoginStatusRef = useRef<LoginStatus | null>(null);
useEffect(() => { useEffect(() => {
if (!authState.hasInitialized || authState.isLoading) return;
const prev = prevLoginStatusRef.current; 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; // 登录态没变
prevLoginStatusRef.current = authState.loginStatus;
if (authState.loginStatus === "notLoggedIn") {
chatDispatch({ type: "ChatLogout" });
router.replace(ROUTES.splash);
return;
}
dispatchAuthLifecycle(authState.loginStatus, chatDispatch); dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
}, [authState.loginStatus, chatDispatch]); }, [
authState.hasInitialized,
authState.isLoading,
authState.loginStatus,
chatDispatch,
router,
]);
return ( return (
<MobileShell> <MobileShell>
+4
View File
@@ -34,6 +34,8 @@ interface AuthState {
errorMessage: string | null; errorMessage: string | null;
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */ /** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
loginStatus: MachineContext["loginStatus"]; loginStatus: MachineContext["loginStatus"];
/** 启动时的本地登录态恢复是否已经完成 */
hasInitialized: MachineContext["hasInitialized"];
/** /**
* 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮派完业务事件后置 true, * 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮派完业务事件后置 true,
* splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` * splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"`
@@ -67,12 +69,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
state.matches("loadingEmailLogin") || state.matches("loadingEmailLogin") ||
state.matches("loadingEmailRegister") || state.matches("loadingEmailRegister") ||
state.matches("loadingOAuth") || state.matches("loadingOAuth") ||
state.matches("initializing") ||
state.matches("loggingOut") || state.matches("loggingOut") ||
state.matches("syncingGoogleBackend") || state.matches("syncingGoogleBackend") ||
state.matches("syncingFacebookBackend") || state.matches("syncingFacebookBackend") ||
state.matches("loadingGuestLogin"), state.matches("loadingGuestLogin"),
errorMessage: state.context.errorMessage, errorMessage: state.context.errorMessage,
loginStatus: state.context.loginStatus, loginStatus: state.context.loginStatus,
hasInitialized: state.context.hasInitialized,
pendingRedirect: state.context.pendingRedirect, pendingRedirect: state.context.pendingRedirect,
}), }),
[state], [state],
+17 -1
View File
@@ -111,6 +111,7 @@ export const authMachine = setup({
target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地) target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
actions: assign({ actions: assign({
loginStatus: ({ event }) => event.output, loginStatus: ({ event }) => event.output,
hasInitialized: true,
// 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图" // 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图"
errorMessage: null, errorMessage: null,
}), }),
@@ -120,6 +121,7 @@ export const authMachine = setup({
actions: assign({ actions: assign({
errorMessage: ({ event }) => errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error), event.error instanceof Error ? event.error.message : String(event.error),
hasInitialized: true,
}), }),
}, },
}, },
@@ -137,6 +139,7 @@ export const authMachine = setup({
// Skip 点击后已在 splash 里立即跳 /chatguest login 不再依赖 pendingRedirect。 // Skip 点击后已在 splash 里立即跳 /chatguest login 不再依赖 pendingRedirect。
pendingRedirect: false, pendingRedirect: false,
errorMessage: null, errorMessage: null,
hasInitialized: true,
}), }),
}, },
onError: { onError: {
@@ -144,6 +147,7 @@ export const authMachine = setup({
actions: assign({ actions: assign({
errorMessage: ({ event }) => errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error), event.error instanceof Error ? event.error.message : String(event.error),
hasInitialized: true,
}), }),
}, },
}, },
@@ -164,6 +168,7 @@ export const authMachine = setup({
loginStatus: ({ event }) => event.output, loginStatus: ({ event }) => event.output,
pendingRedirect: true, // ← 邮箱登录成功 → 跳 pendingRedirect: true, // ← 邮箱登录成功 → 跳
errorMessage: null, errorMessage: null,
hasInitialized: true,
}), }),
}, },
onError: { onError: {
@@ -171,6 +176,7 @@ export const authMachine = setup({
actions: assign({ actions: assign({
errorMessage: ({ event }) => errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error), event.error instanceof Error ? event.error.message : String(event.error),
hasInitialized: true,
}), }),
}, },
}, },
@@ -196,6 +202,7 @@ export const authMachine = setup({
loginStatus: ({ event }) => event.output, loginStatus: ({ event }) => event.output,
pendingRedirect: true, // ← 注册成功 → 跳 pendingRedirect: true, // ← 注册成功 → 跳
errorMessage: null, errorMessage: null,
hasInitialized: true,
}), }),
}, },
onError: { onError: {
@@ -203,6 +210,7 @@ export const authMachine = setup({
actions: assign({ actions: assign({
errorMessage: ({ event }) => errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error), event.error instanceof Error ? event.error.message : String(event.error),
hasInitialized: true,
}), }),
}, },
}, },
@@ -236,12 +244,16 @@ export const authMachine = setup({
src: "logout", src: "logout",
onDone: { onDone: {
target: "idle", target: "idle",
actions: assign(() => initialState), actions: assign(() => ({
...initialState,
hasInitialized: true,
})),
}, },
onError: { onError: {
target: "idle", target: "idle",
actions: assign(({ event }) => ({ actions: assign(({ event }) => ({
...initialState, ...initialState,
hasInitialized: true,
errorMessage: errorMessage:
event.error instanceof Error ? event.error.message : String(event.error), event.error instanceof Error ? event.error.message : String(event.error),
})), })),
@@ -263,6 +275,7 @@ export const authMachine = setup({
loginStatus: ({ event }) => event.output, loginStatus: ({ event }) => event.output,
pendingRedirect: true, // ← OAuth sync 成功 → 跳 pendingRedirect: true, // ← OAuth sync 成功 → 跳
errorMessage: null, errorMessage: null,
hasInitialized: true,
}), }),
}, },
onError: { onError: {
@@ -270,6 +283,7 @@ export const authMachine = setup({
actions: assign({ actions: assign({
errorMessage: ({ event }) => errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error), event.error instanceof Error ? event.error.message : String(event.error),
hasInitialized: true,
}), }),
}, },
}, },
@@ -289,6 +303,7 @@ export const authMachine = setup({
loginStatus: ({ event }) => event.output, loginStatus: ({ event }) => event.output,
pendingRedirect: true, // ← OAuth sync 成功 → 跳 pendingRedirect: true, // ← OAuth sync 成功 → 跳
errorMessage: null, errorMessage: null,
hasInitialized: true,
}), }),
}, },
onError: { onError: {
@@ -296,6 +311,7 @@ export const authMachine = setup({
actions: assign({ actions: assign({
errorMessage: ({ event }) => errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error), event.error instanceof Error ? event.error.message : String(event.error),
hasInitialized: true,
}), }),
}, },
}, },
+3
View File
@@ -15,6 +15,8 @@ export interface AuthState {
errorMessage: string | null; errorMessage: string | null;
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */ /** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
loginStatus: LoginStatus; loginStatus: LoginStatus;
/** 启动时的本地登录态恢复是否已经完成 */
hasInitialized: boolean;
/** /**
* 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮刚派事件 → 置 true * 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮刚派事件 → 置 true
* → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转 * → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转
@@ -35,5 +37,6 @@ export const initialState: AuthState = {
confirmPassword: "", confirmPassword: "",
errorMessage: null, errorMessage: null,
loginStatus: "notLoggedIn", loginStatus: "notLoggedIn",
hasInitialized: false,
pendingRedirect: false, pendingRedirect: false,
}; };