refactor(chat): extract XState actors into chat-machine.actors.ts
- Split chat state machine actors (chatInit, sendMessageHttp, loadMoreHistory, chatWebSocket) into a dedicated module - Add comprehensive console logging across the chat state machine for full trace debugging - Comment out margin on text bubble CSS for layout adjustment - Add debug logging to chat input bar send handler - Clean up redundant comments in auth-screen component
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Chat 状态机:4 个 XState actor
|
||||
*
|
||||
* 从 `chat-machine.ts` 抽出的"Actors"段:
|
||||
* - `chatInitActor`(fromPromise,**仅**游客有意义)
|
||||
* - `sendMessageHttpActor`(fromPromise,**已定义但**未 wire —— 消息**不**真发后端)
|
||||
* - `loadMoreHistoryActor`(fromPromise)
|
||||
* - `chatWebSocketActor`(fromCallback,长生命周期 —— **仅** userSession 期间 invoke)
|
||||
*
|
||||
* 设计:
|
||||
* - 依赖 `chat-machine.helpers.ts`(纯函数 + 数据加载)
|
||||
* - 依赖 `createChatWebSocket`(`@/core/net/chat-websocket`)
|
||||
* - 保留**全链日志**(用户要求"不要将其删除")
|
||||
* - 命名约定:`[chat-machine]` 前缀 —— 与 `chat-machine.ts` 的日志对齐
|
||||
*/
|
||||
|
||||
import { fromPromise, fromCallback } from "xstate";
|
||||
|
||||
import { createChatWebSocket } from "@/core/net/chat-websocket";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import {
|
||||
PAGE_SIZE,
|
||||
chatRepo,
|
||||
localMessagesToUi,
|
||||
readInitData,
|
||||
type InitResult,
|
||||
} from "./chat-machine.helpers";
|
||||
import type { ChatEvent } from "./chat-events";
|
||||
|
||||
// ============================================================// HTTP: one-shot Promise
|
||||
// ============================================================
|
||||
/** 拉初始数据(**仅**游客有意义) */
|
||||
export const chatInitActor = fromPromise<InitResult>(async () => readInitData());
|
||||
|
||||
/**
|
||||
* HTTP 发送消息(**已定义但未** wire —— 消息**不**真发后端)
|
||||
* - 任何 state 转移**未** invoke 此 actor
|
||||
* - 保留全链日志以便排查
|
||||
*/
|
||||
export const sendMessageHttpActor = fromPromise<
|
||||
{ messages: 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: "<actor path>",
|
||||
});
|
||||
console.warn(
|
||||
"[chat-machine] sendMessageHttpActor invoked —— actor 已定义但**未** invoke(消息**不**真发后端)",
|
||||
{ contentLength: input.content.length, contentPreview: input.content.slice(0, 50) },
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
console.log("[chat-machine] sendMessageHttpActor fetching local messages");
|
||||
const local = await chatRepo.getLocalMessages();
|
||||
console.log("[chat-machine] sendMessageHttpActor local messages DONE", {
|
||||
success: local.success,
|
||||
messagesCount: local.success && local.data ? local.data.length : 0,
|
||||
});
|
||||
if (Result.isOk(local) && local.data) {
|
||||
const messages = localMessagesToUi(local.data);
|
||||
console.log("[chat-machine] sendMessageHttpActor done", { messagesCount: messages.length });
|
||||
return { messages };
|
||||
}
|
||||
return { messages: [] };
|
||||
});
|
||||
|
||||
/** 翻历史(pagination) */
|
||||
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: "<actor path>",
|
||||
});
|
||||
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<ChatEvent, { token: string }>(
|
||||
({ sendBack, input, self }) => {
|
||||
console.log("[chat-machine] chatWebSocketActor ENTRY", {
|
||||
hasToken: !!input.token,
|
||||
tokenLength: input.token?.length ?? 0,
|
||||
tokenPrefix: input.token?.slice(0, 10) ?? "EMPTY",
|
||||
selfPath: "<actor path>",
|
||||
});
|
||||
console.log("[chat-machine] chatWebSocketActor creating ChatWebSocket instance");
|
||||
const ws = createChatWebSocket(input.token);
|
||||
console.log("[chat-machine] chatWebSocketActor createChatWebSocket DONE", {
|
||||
wsType: ws.constructor.name,
|
||||
});
|
||||
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)",
|
||||
);
|
||||
ws.disconnect();
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 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