feat(chat): implement WebSocket message sending and integrate with chat state machine
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
|
||||
import { fromPromise, fromCallback } from "xstate";
|
||||
|
||||
import { createChatWebSocket } from "@/core/net/chat-websocket";
|
||||
import { createChatWebSocket, ChatWebSocket } from "@/core/net/chat-websocket";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import {
|
||||
@@ -30,6 +30,28 @@ import {
|
||||
} 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)
|
||||
// ============================================================
|
||||
@@ -196,6 +218,13 @@ export const chatWebSocketActor = fromCallback<ChatEvent, { token: string }>(
|
||||
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,
|
||||
@@ -234,10 +263,62 @@ export const chatWebSocketActor = fromCallback<ChatEvent, { token: string }>(
|
||||
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<void, ...> 稳定(XState v5 已知坑)
|
||||
*/
|
||||
export const sendMessageWsActor = fromPromise<void, { content: string }>(
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user