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:
2026-06-12 11:20:19 +08:00
parent 6394c8ba7b
commit 38f060bbd8
7 changed files with 583 additions and 253 deletions
+197
View File
@@ -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;