/** * Chat 状态机:4 个 XState actor */ import { fromPromise, fromCallback } from "xstate"; import { createChatWebSocket, ChatWebSocket } from "@/core/net/chat-websocket"; import { Result } from "@/utils/result"; import { PAGE_SIZE, chatRepo, localMessagesToUi, readAndSyncHistory, readGuestQuota, sendResponseToUiMessage, } from "./chat-machine.helpers"; import type { ChatEvent } from "./chat-events"; // ============================================================ // 共享 WS 引用(chatWebSocketActor ↔ sendMessageWsActor) // ============================================================ /** * 活跃 ChatWebSocket 单例引用(module-level)。 * - `chatWebSocketActor` 在 connect 后写入、cleanup 时清空 * - `sendMessageWsActor` 读取并调 `ws.sendMessage(content)` * * 为什么用 module-level 而非 context.wsRef: * 1) `ChatWebSocket` 是带回调 + 重连 timer 的非可序列化对象,放进 * XState context 会污染快照、警告 non-serializable * 2) userSession 同一时间只可能有一个活跃 WS(parent-level invoke), * module 单例的语义和"userSession 内一个活跃连接"一一对应 * 3) 不需要重构 ChatWebSocket 类的对外 API */ let activeChatWebSocket: ChatWebSocket | null = null; /** 暴露给 `sendMessageWsActor` 读取(测试也可 import) */ export function getActiveChatWebSocket(): ChatWebSocket | null { return activeChatWebSocket; } // ============================================================ // Init 任务 1:拉游客配额(fromPromise,纯 local) // ============================================================ /** * 拉游客日配 + 总配(仅 guestSession 期间 invoke) * - 失败返 `{ remaining: 0, total: 0 }`(不抛,让 UI 还是能进 ready) * - onDone 在 state machine 里赋 guestRemainingQuota + guestTotalQuota + 设 quotaLoaded: true */ export const loadQuotaActor = fromPromise<{ remaining: number; total: number; }>(async () => { console.log("[chat-machine] loadQuotaActor ENTRY"); const result = await readGuestQuota(); console.log("[chat-machine] loadQuotaActor DONE", result); return result; }); // ============================================================ // Init 任务 2:拉 history 3 步流(fromPromise,返回最终结果) // ============================================================ /** * local → network → save network to local 3 步流(返回 network 最终 messages) * - 选方案 A(fromPromise):UI 不会看到 local 中间态,直接显示 network 结果 * - 内部 3 步走 helper `readAndSyncHistory`(日志全在 helper 里) * - 失败返 local(退而求其次) */ export const loadHistoryActor = fromPromise<{ messages: UiMessage[]; hasMore: boolean; newOffset: number; localOverwritten: boolean; localCount: number; networkCount: number; }>(async () => { console.log("[chat-machine] loadHistoryActor ENTRY"); const result = await readAndSyncHistory(); console.log("[chat-machine] loadHistoryActor DONE", { finalCount: result.messages.length, localCount: result.localCount, networkCount: result.networkCount, hasMore: result.hasMore, localOverwritten: result.localOverwritten, }); return result; }); // ============================================================ // HTTP: one-shot Promise // ============================================================ /** * HTTP 发送消息(一次发送 = 一次返回) * - 后端响应就是 AI 回复(`ChatSendResponse.reply: string`) * - 不再调 `getLocalMessages()`(多此一举) * - 返回 `{ reply: UiMessage }` —— `sending.onDone` 把它追加到 `context.messages` */ export const sendMessageHttpActor = fromPromise< { reply: UiMessage }, { content: string } >(async ({ input, self }) => { console.log("[chat-machine] sendMessageHttpActor ENTRY", { contentLength: input.content.length, contentPreview: input.content.slice(0, 50), selfId: self.id, selfPath: "", }); console.log("[chat-machine] sendMessageHttpActor calling chatRepo.sendMessage"); const result = await chatRepo.sendMessage(input.content); console.log("[chat-machine] sendMessageHttpActor chatRepo.sendMessage DONE", { success: result.success, error: result.success ? null : Result.isErr(result) ? result.error : null, }); if (Result.isErr(result)) { console.error("[chat-machine] sendMessageHttpActor result isErr, throwing"); throw result.error; } // 一次发送 = 一次返回 —— 直接转 reply → UiMessage console.log("[chat-machine] sendMessageHttpActor converting reply to UiMessage", { replyLength: result.data.reply.length, replyPreview: result.data.reply.slice(0, 50), messageId: result.data.messageId, timestamp: result.data.timestamp, }); const reply = sendResponseToUiMessage(result.data); console.log("[chat-machine] sendMessageHttpActor done", { replyContentLength: reply.content.length, }); return { reply }; }); /** 翻历史(pagination)—— 不走 local-first,纯 server fetch */ export const loadMoreHistoryActor = fromPromise< { messages: UiMessage[]; hasMore: boolean; newOffset: number }, { offset: number } >(async ({ input, self }) => { console.log("[chat-machine] loadMoreHistoryActor ENTRY", { inputOffset: input.offset, pageSize: PAGE_SIZE, selfPath: "", }); console.log("[chat-machine] loadMoreHistoryActor calling chatRepo.getHistory", { pageSize: PAGE_SIZE, offset: input.offset, }); const result = await chatRepo.getHistory(PAGE_SIZE, input.offset); console.log("[chat-machine] loadMoreHistoryActor chatRepo.getHistory DONE", { success: result.success, error: result.success ? null : Result.isErr(result) ? result.error : null, responseType: result.success ? typeof result.data : "err", }); if (Result.isErr(result)) { console.error("[chat-machine] loadMoreHistoryActor result isErr, throwing", { error: result.error, }); throw result.error; } console.log("[chat-machine] loadMoreHistoryActor result data", { rawMessagesCount: result.data.messages.length, }); const page = localMessagesToUi(result.data.messages); console.log("[chat-machine] loadMoreHistoryActor AFTER UiMessage mapping", { uiMessagesCount: page.length, }); const hasMore = page.length >= PAGE_SIZE; const newOffset = input.offset + page.length; console.log("[chat-machine] loadMoreHistoryActor result", { pageSize: page.length, hasMore, newOffset, }); return { messages: page, hasMore, newOffset, }; }); // ============================================================ // WebSocket: long-lived callback // ============================================================ /** * 长生命周期 WebSocket(仅 `userSession` 期间 invoke) * - 启动:连 WS,事件 → sendBack 派回机器 * - 停止(cleanup):disconnect * - 游客不 invoke 此 actor(业务事实"游客不连 WS") * * 之所以用 fromCallback 而非 fromPromise: * - WS 是长连接,不是一次性 Promise * - fromCallback 提供 `sendBack` 回调,actor 内部事件 → 机器 * - cleanup 函数 → XState 在 parent state exit 时自动调用 */ export const chatWebSocketActor = fromCallback( ({ sendBack, input }) => { console.log("[chat-machine] chatWebSocketActor ENTRY", { hasToken: !!input.token, tokenLength: input.token?.length ?? 0, tokenPrefix: input.token?.slice(0, 10) ?? "EMPTY", selfPath: "", }); console.log("[chat-machine] chatWebSocketActor creating ChatWebSocket instance"); const ws = createChatWebSocket(input.token); console.log("[chat-machine] chatWebSocketActor createChatWebSocket DONE", { wsType: ws.constructor.name, }); // 写入 module-level 共享引用 —— sendMessageWsActor 借此调 ws.sendMessage() activeChatWebSocket = ws; console.log("[chat-machine] chatWebSocketActor activeChatWebSocket SET", { isConnected: ws.isConnected, }); ws.onConnected = (userId) => { console.log("[chat-machine] WS onConnected", { userId, userIdPrefix: userId?.slice(0, 8) ?? "EMPTY", }); sendBack({ type: "ChatWebSocketConnected", userId }); }; ws.onSentence = (p) => { console.log("[chat-machine] WS onSentence", { index: p.index, total: p.total, done: p.done, textLength: p.text.length, textPreview: p.text.slice(0, 50), textSuffix: p.text.slice(-20), }); sendBack({ type: "ChatAISentenceReceived", index: p.index, text: p.text, total: p.total, done: p.done, }); }; ws.onError = (msg) => { console.error("[chat-machine] WS onError", { errorMessage: msg, errorLength: msg?.length ?? 0, }); sendBack({ type: "ChatWebSocketError", errorMessage: msg }); }; console.log("[chat-machine] chatWebSocketActor calling ws.connect()"); ws.connect(); console.log("[chat-machine] chatWebSocketActor ws.connect() called"); return () => { console.log( "[chat-machine] chatWebSocketActor CLEANUP (parent state exit / logout / cross-transition / unmount)", ); // 清空 module-level 引用 —— 严格用 `=== ws` 判等,避免覆盖更新的实例 if (activeChatWebSocket === ws) { activeChatWebSocket = null; console.log("[chat-machine] chatWebSocketActor activeChatWebSocket CLEARED"); } ws.disconnect(); }; }, ); // ============================================================ // WebSocket: per-send callback // ============================================================ /** * WS 发送消息(userSession.sendingViaWs 期间 invoke) * - 走 `activeChatWebSocket.sendMessage(content)` * - 成功后 AI 回复通过 `chatWebSocketActor` 的 `onSentence` → `ChatAISentenceReceived` 流回 * - 失败抛错 → state machine 的 `onError` 兜底(清 isReplyingAI、回 ready) * * 与 sendMessageHttpActor 的区别: * - sendMessageHttpActor: 一次发送 = 一次返回(HTTP response = AI reply) * - sendMessageWsActor: 一次发送 = 0 次直接返回(WS 异步流式回 reply,actor 本身不返 reply) * * 选 fromPromise 而非 fromCallback: * - 本 actor 是 fire-and-forget —— sync 内同步执行 ws.sendMessage,立即 resolve * - actor 本身没有"完成"语义(done 来自 WS 事件流,不来自这个 actor) * - 抛错走 onError 通道 * - fromPromise 的 input type 推断比 fromCallback 稳定(XState v5 已知坑) */ export const sendMessageWsActor = fromPromise( async ({ input }) => { console.log("[chat-machine] sendMessageWsActor ENTRY", { contentLength: input.content.length, contentPreview: input.content.slice(0, 50), }); const ws = getActiveChatWebSocket(); if (!ws) { console.error("[chat-machine] sendMessageWsActor no activeChatWebSocket"); throw new Error("[chat-machine] sendMessageWs: no active WebSocket (actor not running)"); } console.log("[chat-machine] sendMessageWsActor calling ws.sendMessage", { isConnected: ws.isConnected, }); const ok = ws.sendMessage(input.content); if (!ok) { console.error( "[chat-machine] sendMessageWsActor ws.sendMessage returned false (not OPEN)", ); throw new Error("[chat-machine] sendMessageWs: WebSocket not OPEN"); } console.log( "[chat-machine] sendMessageWsActor ws.sendMessage OK, awaiting AI sentence stream", ); // resolve 不返值 —— AI reply 由 chatWebSocketActor 的 ChatAISentenceReceived 流回 }, ); // Re-export `UiMessage` type for the chat-machine.ts public API type UiMessage = import("@/models/chat/ui-message").UiMessage;