refactor(chat): decouple chat state from auth and gate quota alerts to guests
- Remove ChatAuthStatusChanged dispatch from auth login flow; chat screen now subscribes directly to auth state machine (loginStatus) to derive isGuest and drive WS lifecycle - Gate quota-exceeded and warning dialogs to guests only (non-guests have no message limit) - Dispatch ChatLoadMoreHistory for non-guest initial load vs ChatInit for guests - Fix AppConstants import path from @/core/constants/app_constants to @/core/app_constants - Add defensive isGuest checks in quota effects to prevent non-guest quota triggers
This commit is contained in:
@@ -8,8 +8,8 @@
|
||||
* 兼容性:保持原 `useChatState()` / `useChatDispatch()` API。
|
||||
* 内部将 XState 状态机快照映射回原 `ChatState` 形状。
|
||||
*
|
||||
* 注:WebSocket 长生命周期 actor 暂未集成到状态机(保留在 chat-side-effects.ts
|
||||
* 由外部 chat-side-effects 模块管理),待下一轮迁移。
|
||||
* 注:WebSocket 长生命周期 actor **由 chat-screen.tsx 监听 auth 状态机驱动**——
|
||||
* 鉴权解耦(loginStatus 在 auth,chat 机器**不**感知鉴权)。
|
||||
*/
|
||||
import {
|
||||
type Dispatch,
|
||||
@@ -24,13 +24,17 @@ import { chatMachine } from "./chat-machine";
|
||||
import type { ChatEvent, ChatState as MachineContext } from "./chat-machine";
|
||||
|
||||
/**
|
||||
* 对外暴露的 State 形状(保持与原 ChatState 兼容)
|
||||
* 对外暴露的 State 形状
|
||||
*
|
||||
* **isGuest 字段已删** —— 由 chat-screen 派生自 `auth.loginStatus === "guest"`。
|
||||
* 消费方(chat-screen)通过 `useAuthState()` 取 loginStatus,本地派生 isGuest。
|
||||
*/
|
||||
interface ChatState {
|
||||
messages: MachineContext["messages"];
|
||||
isReplyingAI: boolean;
|
||||
isGuest: boolean;
|
||||
/** 游客剩余配额(次/天)—— 业务展示用,鉴权判断由 chat-screen 派生 loginStatus */
|
||||
guestRemainingQuota: number;
|
||||
/** 游客总配额 */
|
||||
guestTotalQuota: number;
|
||||
quotaExceededTrigger: number;
|
||||
isLoadingMore: boolean;
|
||||
@@ -53,7 +57,6 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
||||
() => ({
|
||||
messages: state.context.messages,
|
||||
isReplyingAI: state.context.isReplyingAI,
|
||||
isGuest: state.context.isGuest,
|
||||
guestRemainingQuota: state.context.guestRemainingQuota,
|
||||
guestTotalQuota: state.context.guestTotalQuota,
|
||||
quotaExceededTrigger: state.context.quotaExceededTrigger,
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
* Chat 状态机:事件联合
|
||||
*
|
||||
* 事件名与原 Dart ChatEvent 对齐。
|
||||
*
|
||||
* 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已**删**。
|
||||
* 鉴权状态变化(loginStatus)由 chat-screen 通过 `useAuthState()` 订阅,
|
||||
* chat 机器**不**感知鉴权。
|
||||
*/
|
||||
export type ChatEvent =
|
||||
| { type: "ChatInit" }
|
||||
@@ -19,5 +23,4 @@ export type ChatEvent =
|
||||
}
|
||||
| { type: "ChatWebSocketError"; errorMessage: string }
|
||||
| { type: "ChatWebSocketConnected"; userId: string }
|
||||
| { type: "ChatQuotaExceeded" }
|
||||
| { type: "ChatAuthStatusChanged" };
|
||||
| { type: "ChatQuotaExceeded" };
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
* - HTTP 操作用 `fromPromise` actor(一次性 Promise)
|
||||
* - 保持事件类型名与原 Dart 一致,便于业务层迁移对照
|
||||
* - actions 全部 inline 在 setup() 中(确保类型推断正确)
|
||||
*
|
||||
* **鉴权解耦**:
|
||||
* chat 机器**不**感知鉴权状态 —— `isGuest` 字段已删(由 chat-screen 派生自 auth.loginStatus)。
|
||||
* WebSocket 管理也由 chat-screen 监听 auth 状态机后驱动。
|
||||
*
|
||||
* **配额(**仅**游客)**:
|
||||
* - `guestRemainingQuota` / `guestTotalQuota` **仅**游客有意义
|
||||
* - 非游客"无消息限制"(业务事实)—— 永不被赋值
|
||||
* - chat-screen 派 `ChatInit` 时(**只**在 guest)→ onDone 条件赋
|
||||
* - `appendUserMessage` 减 `Math.max(0, q - 1)` —— 非游客 0-1=0 天然 no-op
|
||||
*/
|
||||
import { setup, fromPromise, assign } from "xstate";
|
||||
|
||||
@@ -85,6 +95,8 @@ interface InitResult {
|
||||
|
||||
async function readInitData(): Promise<InitResult> {
|
||||
const chatStorage = ChatStorage.getInstance();
|
||||
// isGuest 在 chat 业务层仅用于"是否加载本地历史" + "onDone 条件赋配额" —— **不**是鉴权信号
|
||||
// 鉴权判断已上提到 chat-screen 派生
|
||||
const isGuest = !AuthStorage.getInstance().hasLoginToken();
|
||||
|
||||
const [dailyResult, totalResult, localResult] = await Promise.all([
|
||||
@@ -95,13 +107,14 @@ async function readInitData(): Promise<InitResult> {
|
||||
|
||||
return {
|
||||
isGuest,
|
||||
// fallback 0 —— 非游客路径用不到;onDone 会**条件**赋
|
||||
guestRemainingQuota: mapQuotaResult(
|
||||
dailyResult as Result<{ remaining: number } | null>,
|
||||
40,
|
||||
0,
|
||||
),
|
||||
guestTotalQuota: mapTotalQuotaResult(
|
||||
totalResult as Result<number | null>,
|
||||
50,
|
||||
0,
|
||||
),
|
||||
localMessages:
|
||||
localResult && Result.isOk(localResult) && localResult.data
|
||||
@@ -145,8 +158,7 @@ const loadMoreHistoryActor = fromPromise<
|
||||
};
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Machine
|
||||
// ============================================================// Machine
|
||||
// ============================================================
|
||||
export const chatMachine = setup({
|
||||
types: {
|
||||
@@ -171,6 +183,8 @@ export const chatMachine = setup({
|
||||
},
|
||||
],
|
||||
isReplyingAI: true,
|
||||
// 游客减配额;非游客 0 - 1 = max(0, -1) = 0 —— **天然 no-op**(业务事实"非游客无限制")
|
||||
guestRemainingQuota: Math.max(0, context.guestRemainingQuota - 1),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -187,6 +201,8 @@ export const chatMachine = setup({
|
||||
},
|
||||
],
|
||||
isReplyingAI: true,
|
||||
// 游客减配额;非游客 no-op
|
||||
guestRemainingQuota: Math.max(0, context.guestRemainingQuota - 1),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -223,10 +239,6 @@ export const chatMachine = setup({
|
||||
return { messages, isReplyingAI: false };
|
||||
}),
|
||||
|
||||
refreshAuthStatus: assign(() => ({
|
||||
isGuest: !AuthStorage.getInstance().hasLoginToken(),
|
||||
})),
|
||||
|
||||
incrementQuotaExceeded: assign(({ context }) => ({
|
||||
quotaExceededTrigger: context.quotaExceededTrigger + 1,
|
||||
})),
|
||||
@@ -248,9 +260,6 @@ export const chatMachine = setup({
|
||||
ChatSendImage: {
|
||||
actions: "appendUserImage",
|
||||
},
|
||||
ChatAuthStatusChanged: {
|
||||
actions: "refreshAuthStatus",
|
||||
},
|
||||
ChatAISentenceReceived: {
|
||||
actions: "appendOrUpdateAISentence",
|
||||
},
|
||||
@@ -269,11 +278,18 @@ export const chatMachine = setup({
|
||||
src: "chatInit",
|
||||
onDone: {
|
||||
target: "ready",
|
||||
actions: assign({
|
||||
messages: ({ event }) => event.output.localMessages,
|
||||
isGuest: ({ event }) => event.output.isGuest,
|
||||
guestRemainingQuota: ({ event }) => event.output.guestRemainingQuota,
|
||||
guestTotalQuota: ({ event }) => event.output.guestTotalQuota,
|
||||
// **条件赋**:仅 isGuest 时初始化配额(业务事实"非游客无消息限制")
|
||||
// - 游客:messages + quota 都更新
|
||||
// - 非游客:仅 messages(local 空),quota 保持 default 0(不更新其值)
|
||||
actions: assign(({ event }) => {
|
||||
const updates: Partial<ChatState> = {
|
||||
messages: event.output.localMessages,
|
||||
};
|
||||
if (event.output.isGuest) {
|
||||
updates.guestRemainingQuota = event.output.guestRemainingQuota;
|
||||
updates.guestTotalQuota = event.output.guestTotalQuota;
|
||||
}
|
||||
return updates;
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
@@ -293,9 +309,6 @@ export const chatMachine = setup({
|
||||
},
|
||||
ChatLoadMoreHistory: "loadingMoreHistory",
|
||||
ChatScreenInvisible: "idle",
|
||||
ChatAuthStatusChanged: {
|
||||
actions: "refreshAuthStatus",
|
||||
},
|
||||
ChatAISentenceReceived: {
|
||||
actions: "appendOrUpdateAISentence",
|
||||
},
|
||||
|
||||
@@ -2,14 +2,26 @@
|
||||
* Chat 状态机:State 形状 + 初始值
|
||||
*
|
||||
* 注: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 && ...)` 天然过滤
|
||||
*/
|
||||
import type { UiMessage } from "@/models/chat/ui-message";
|
||||
|
||||
export interface ChatState {
|
||||
messages: UiMessage[];
|
||||
isReplyingAI: boolean;
|
||||
isGuest: boolean;
|
||||
/**
|
||||
* 游客剩余配额(次/天)—— **仅**游客业务展示用
|
||||
* - default 0 = "未初始化" / "已耗尽"(同码,靠 `isGuest` 区分)
|
||||
* - 非游客:永不被赋值(业务事实"无消息限制")
|
||||
*/
|
||||
guestRemainingQuota: number;
|
||||
/** 游客总配额(仅游客展示用) */
|
||||
guestTotalQuota: number;
|
||||
quotaExceededTrigger: number;
|
||||
isLoadingMore: boolean;
|
||||
@@ -20,9 +32,8 @@ export interface ChatState {
|
||||
export const initialState: ChatState = {
|
||||
messages: [],
|
||||
isReplyingAI: false,
|
||||
isGuest: true,
|
||||
guestRemainingQuota: 40,
|
||||
guestTotalQuota: 50,
|
||||
guestRemainingQuota: 0,
|
||||
guestTotalQuota: 0,
|
||||
quotaExceededTrigger: 0,
|
||||
isLoadingMore: false,
|
||||
hasMore: true,
|
||||
|
||||
Reference in New Issue
Block a user