/** * Chat 状态机(XState v5) * * 鉴权解耦(事件驱动): * chat 机器不感知鉴权 —— 由 派生 loginStatus * ChatAuthSync 派发登录态生命周期事件: * - `ChatGuestLogin` → 游客会话 * - `ChatUserLogin { token }` → 其他登录用户会话 * - `ChatLogout` → 正式登录用户登出 * * 登录态流转约束: * - 未登录:可以进入游客登录 / 其他登录。 * - 游客登录:只可以升级为其他登录,不响应退出。 * - 其他登录:可以退出,不响应游客登录。 * * 状态结构(parent state 模式): * - `idle`:屏没挂 / 登出 * - `guestSession`(parent):游客会话 * - `userSession`(parent):其他登录用户会话 * * init 任务: * - guestSession.initializing:loadHistory * - userSession.initializing:loadHistory * - 每个任务都是独立 actor,不互相等待(除非 `always` barrier) * * 消息发送路径(业务事实): * - `ChatSendMessage` 只负责即时追加用户消息 + 入队,不进入 sending 子状态 * - guestSession / userSession:队列串行走 HTTP,限制以后端 `lockDetail` 为准 * - `pendingReplyCount` 跟踪待回复数量,`isReplyingAI` 由计数派生 * * 配额: * - 前端不再处理本地消息额度;游客 / 注册用户均以后端响应为准。 * - 后端返回 `lockDetail.reason="daily_limit"` 且需要升级时展示会员引导。 * */ import { setup, assign, sendTo } from "xstate"; import { todayString, Logger } from "@/utils"; import { ChatState, initialState } from "./chat-state"; import type { ChatEvent } from "./chat-events"; import { applyHttpSendOutput, beginPendingReply, finishPendingReply, } from "./chat-machine.helpers"; import { loadHistoryActor, sendMessageHttpActor, loadMoreHistoryActor, httpMessageQueueActor, } from "./chat-machine.actors"; const log = new Logger("StoresChatChatMachine"); // 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变 export type { ChatState } from "./chat-state"; export { initialState } from "./chat-state"; export type { ChatEvent } from "./chat-events"; // ============================================================ // Machine // ============================================================ export const chatMachine = setup({ types: { context: {} as ChatState, events: {} as ChatEvent, }, actors: { loadHistory: loadHistoryActor, sendMessageHttp: sendMessageHttpActor, loadMoreHistory: loadMoreHistoryActor, httpMessageQueue: httpMessageQueueActor, }, actions: { enqueueMessage: sendTo("messageQueue", ({ event }) => event), startGuestSession: assign(() => ({ ...initialState, })), startUserSession: assign(() => ({ ...initialState, })), clearChatSession: assign(() => ({ ...initialState, })), appendGuestUserMessage: assign(({ context, event }) => { if (event.type !== "ChatSendMessage") return {}; const today = todayString(); log.debug("[chat-machine] appendGuestUserMessage", { contentLength: event.content.length, contentPreview: event.content.slice(0, 50), quotaMode: "server controlled", isReplyingAI: context.isReplyingAI, }); return { messages: [ ...context.messages, { content: event.content, isFromAI: false, date: today, }, ], upgradePromptVisible: false, upgradeReason: null, upgradeHint: null, upgradeDetail: null, }; }), appendUserMessage: assign(({ context, event }) => { if (event.type !== "ChatSendMessage") return {}; const today = todayString(); log.debug("[chat-machine] appendUserMessage", { contentLength: event.content.length, contentPreview: event.content.slice(0, 50), isReplyingAI: context.isReplyingAI, }); return { messages: [ ...context.messages, { content: event.content, isFromAI: false, date: today, }, ], upgradePromptVisible: false, upgradeReason: null, upgradeHint: null, upgradeDetail: null, }; }), appendGuestUserImage: assign(({ context, event }) => { if (event.type !== "ChatSendImage") return {}; const today = todayString(); log.debug("[chat-machine] appendGuestUserImage", { oldMessagesCount: context.messages.length, quotaMode: "server controlled", }); return { messages: [ ...context.messages, { content: "[Image]", isFromAI: false, date: today, imageUrl: event.imageBase64, }, ], ...beginPendingReply(context), upgradePromptVisible: false, upgradeReason: null, upgradeHint: null, upgradeDetail: null, }; }), appendUserImage: assign(({ context, event }) => { if (event.type !== "ChatSendImage") return {}; const today = todayString(); log.debug("[chat-machine] appendUserImage", { oldMessagesCount: context.messages.length, }); return { messages: [ ...context.messages, { content: "[Image]", isFromAI: false, date: today, imageUrl: event.imageBase64, }, ], ...beginPendingReply(context), upgradePromptVisible: false, upgradeReason: null, upgradeHint: null, upgradeDetail: null, }; }), appendQueuedSendErrorMessage: assign(({ context }) => { const messages = [ ...context.messages, { content: "Something went wrong. Try sending again?", isFromAI: true, date: todayString(), }, ]; return { messages, ...finishPendingReply(context) }; }), markQueuedSendStarted: assign(({ context }) => beginPendingReply(context)), applyQueuedHttpOutput: assign(({ context, event }) => { if (event.type !== "ChatQueuedHttpDone") return {}; return applyHttpSendOutput(context, event.output); }), }, }).createMachine({ id: "chat", initial: "idle", context: initialState, states: { idle: { on: { ChatGuestLogin: { target: "#chat.guestSession", actions: "startGuestSession", }, ChatUserLogin: { target: "#chat.userSession", actions: "startUserSession", }, }, }, guestSession: { invoke: { id: "messageQueue", src: "httpMessageQueue", }, on: { ChatUserLogin: { target: "#chat.userSession", actions: "startUserSession", }, ChatQueuedSendStarted: { actions: "markQueuedSendStarted", }, ChatQueuedHttpDone: { actions: "applyQueuedHttpOutput", }, ChatQueuedSendError: { actions: "appendQueuedSendErrorMessage", }, }, initial: "initializing", states: { initializing: { // 本地游客额度逻辑已停用:只等待历史加载完成。 always: [ { target: "ready", guard: ({ context }) => context.historyLoaded, }, ], invoke: { id: "loadHistory", src: "loadHistory", onDone: { actions: assign(({ event }) => ({ messages: event.output.messages, hasMore: event.output.hasMore, historyOffset: event.output.newOffset, historyLoaded: true, })), }, onError: { // 失败也标 loaded,不卡 init(让 UI 还是能进 ready,屏幕可能空) actions: assign({ historyLoaded: true, }), }, }, }, ready: { on: { ChatSendMessage: { actions: ["appendGuestUserMessage", "enqueueMessage"], guard: ({ event }) => event.content.trim().length > 0, }, ChatSendImage: { actions: "appendGuestUserImage", target: "sending", }, // 游客不支持翻页历史,发送消息统一走 HTTP 队列。 }, }, sending: { invoke: { src: "sendMessageHttp", input: ({ event }) => ({ content: event.type === "ChatSendMessage" ? event.content : "", }), onDone: { target: "ready", actions: assign(({ context, event }) => applyHttpSendOutput(context, event.output), ), }, onError: { target: "ready", actions: assign({ isReplyingAI: false }), }, }, }, }, }, userSession: { invoke: { id: "messageQueue", src: "httpMessageQueue", }, on: { ChatLogout: { target: "#chat.idle", actions: "clearChatSession", }, ChatUserLogin: { target: "#chat.userSession", reenter: true, actions: "startUserSession", }, ChatQueuedSendStarted: { actions: "markQueuedSendStarted", }, ChatQueuedHttpDone: { actions: "applyQueuedHttpOutput", }, ChatQueuedSendError: { actions: "appendQueuedSendErrorMessage", }, }, initial: "initializing", states: { initializing: { invoke: { src: "loadHistory", onDone: { target: "ready", actions: assign(({ event }) => ({ messages: event.output.messages, isLoadingMore: false, hasMore: event.output.hasMore, historyOffset: event.output.newOffset, historyLoaded: true, })), }, onError: { target: "ready", actions: assign({ isLoadingMore: false, historyLoaded: true, }), }, }, }, ready: { on: { ChatSendMessage: { guard: ({ event }) => event.content.trim().length > 0, actions: ["appendUserMessage", "enqueueMessage"], }, ChatSendImage: { actions: "appendUserImage", }, ChatLoadMoreHistory: { target: "loadingMore", }, }, }, sendingViaHttp: { invoke: { src: "sendMessageHttp", input: ({ event }) => ({ content: event.type === "ChatSendMessage" ? event.content : "", }), onDone: { target: "ready", actions: assign(({ context, event }) => applyHttpSendOutput(context, event.output), ), }, onError: { target: "ready", actions: assign({ isReplyingAI: false }), }, }, }, loadingMore: { invoke: { src: "loadMoreHistory", input: ({ context }) => ({ offset: context.historyOffset }), onDone: { target: "ready", actions: assign(({ context, event }) => ({ messages: [...event.output.messages, ...context.messages], isLoadingMore: false, hasMore: event.output.hasMore, historyOffset: event.output.newOffset, })), }, onError: { target: "ready", actions: assign({ isLoadingMore: false }), }, }, }, }, }, }, }); export type ChatMachine = typeof chatMachine;