chore(chat): refactor chat init into separate quota and history actors
Split `chatInitActor` into `loadQuotaActor` (guest quota fetch) and `loadHistoryActor` (local → network → save sync), and add `quotaLoaded` / `historyLoaded` state flags so the UI can display loading status during the new local-first history hydration flow. Also drop the now-unused `ChatInit` event and pipe Next.js stdout/stderr to a persistent log file in the post-receive hook for easier troubleshooting.
This commit is contained in:
+120
-104
@@ -18,17 +18,27 @@
|
||||
* **状态结构**(parent state 模式):
|
||||
* - `idle`:屏没挂 / 登出
|
||||
* - `guestSession`(**parent**):游客会话 —— **不** invoke WS
|
||||
* - `initializing`:invoke chatInit(拉本地 + 初始化配额)
|
||||
* - `ready`:用户聊天 + 翻历史
|
||||
* - `initializing`:**双** invoke + `always` barrier
|
||||
* - `loadQuotaActor`(**纯** local,ChatStorage **日**配 + 总**配**)
|
||||
* - `loadHistoryActor`(local → network → save 3 步)
|
||||
* - 2 **个** `quotaLoaded` / `historyLoaded` **都** true **才**进** ready
|
||||
* - `ready`:用户聊天 + 看消息
|
||||
* - `sending`:invoke sendMessageHttpActor(HTTP 发送消息)
|
||||
* - `loadingMore`:invoke loadMoreHistory(翻历史)
|
||||
* - ~~`loadingMore`~~:**已**删**(游客**无**服**务**端** history,**不**支持翻**页**)
|
||||
* - `userSession`(**parent**):非游客会话 —— **invoke WS**(持久)
|
||||
* - `initializing`:invoke loadMoreHistory(拉服务器端首屏)
|
||||
* - `ready`:用户聊天 + 翻历史
|
||||
* - 父级 invoke `chatWebSocket`(**一**进**来****就**连)
|
||||
* - `initializing`:invoke `loadHistory` + `always` barrier
|
||||
* - WS **已**连** + history **加**载**完**成** → ready
|
||||
* - `ready`:用户聊天 + 看消息
|
||||
* - `sending`:invoke sendMessageHttpActor(HTTP fallback —— WS 真发**未** wire)
|
||||
* - `loadingMore`:invoke loadMoreHistory(翻历史)
|
||||
* - `loadingMore`:invoke loadMoreHistory(**翻**页**,**保**留**)
|
||||
* WS 在 `userSession` parent 状态 invoke —— 跨 initializing/ready/sending/loadingMore 持续
|
||||
*
|
||||
* **init 双任务**(**核**心**重**构**):
|
||||
* - guestSession.initializing:loadQuota + loadHistory **并**行**(`invoke: [...]` **数**组**)
|
||||
* - userSession.initializing:loadHistory + parent-level chatWebSocket **并**行
|
||||
* - **每**个**任务**都**是**独**立** actor,**不**互**相**等**待**(**除**非** `always` barrier)
|
||||
*
|
||||
* **消息发送路径**(**业务事实**):
|
||||
* - `wsConnected: true`(**仅** userSession 期间)→ **走 WS**,**不**消耗次数(**仅**配额规则)
|
||||
* - `wsConnected: false` → **走 HTTP**,消耗次数(**仅**游客有意义 —— 非游客 0-1=max(0,-1)=0 天然 no-op)
|
||||
@@ -41,12 +51,12 @@
|
||||
*
|
||||
* **文件结构**:
|
||||
* - `chat-machine.ts`(本文件)—— **只**含 setup + createMachine(actions + states)
|
||||
* - `chat-machine.helpers.ts` —— 纯函数 + 数据加载
|
||||
* - `chat-machine.actors.ts` —— 4 个 XState actor(含全链日志)
|
||||
* - `chat-machine.helpers.ts` —— 纯函数 + 数据加载(readGuestQuota + readAndSyncHistory)
|
||||
* - `chat-machine.actors.ts` —— 5 个 XState actor(loadQuota + loadHistory + sendMessage + loadMoreHistory + chatWebSocket)
|
||||
*
|
||||
* **调试日志**(保留 —— 用户要求"不要将其删除"):
|
||||
* - 命名约定:`[<file>]` 前缀标识来源
|
||||
* - 3 actor **全链日志**(ENTRY / API / DONE / 关键决策)—— 排查 actor **没**被调时**一眼**看到
|
||||
* - 4 actor **全链日志**(ENTRY / API / DONE / 关键决策)—— 排查 actor **没**被调时**一眼**看到
|
||||
*/
|
||||
|
||||
import { setup, assign } from "xstate";
|
||||
@@ -56,7 +66,8 @@ import { formatDate } from "@/utils/date";
|
||||
import { ChatState, initialState } from "./chat-state";
|
||||
import type { ChatEvent } from "./chat-events";
|
||||
import {
|
||||
chatInitActor,
|
||||
loadQuotaActor,
|
||||
loadHistoryActor,
|
||||
sendMessageHttpActor,
|
||||
loadMoreHistoryActor,
|
||||
chatWebSocketActor,
|
||||
@@ -82,7 +93,8 @@ export const chatMachine = setup({
|
||||
events: {} as ChatEvent,
|
||||
},
|
||||
actors: {
|
||||
chatInit: chatInitActor,
|
||||
loadQuota: loadQuotaActor,
|
||||
loadHistory: loadHistoryActor,
|
||||
sendMessageHttp: sendMessageHttpActor,
|
||||
loadMoreHistory: loadMoreHistoryActor,
|
||||
chatWebSocket: chatWebSocketActor,
|
||||
@@ -223,39 +235,76 @@ export const chatMachine = setup({
|
||||
},
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// guestSession(游客会话 —— **不**连 WS,2 **个** init 任务**并**行**)
|
||||
// ========================================================================
|
||||
guestSession: {
|
||||
initial: "initializing",
|
||||
states: {
|
||||
initializing: {
|
||||
invoke: {
|
||||
src: "chatInit",
|
||||
onDone: {
|
||||
// "always" barrier:两**个**任务**都**完**成**才**进** ready
|
||||
always: [
|
||||
{
|
||||
target: "ready",
|
||||
actions: assign(({ event }) => {
|
||||
const updates: Partial<ChatState> = {
|
||||
messages: event.output.localMessages,
|
||||
};
|
||||
if (event.output.isGuest) {
|
||||
updates.guestRemainingQuota = event.output.guestRemainingQuota;
|
||||
updates.guestTotalQuota = event.output.guestTotalQuota;
|
||||
}
|
||||
console.log("[chat-machine] guestSession.initializing.onDone", {
|
||||
messagesCount: event.output.localMessages.length,
|
||||
isGuest: event.output.isGuest,
|
||||
guestRemainingQuota: updates.guestRemainingQuota ?? 0,
|
||||
guestTotalQuota: updates.guestTotalQuota ?? 0,
|
||||
});
|
||||
return updates;
|
||||
}),
|
||||
guard: ({ context }) => context.quotaLoaded && context.historyLoaded,
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: ({ event }) =>
|
||||
console.error("[chat-machine] guestSession.initializing.onError", {
|
||||
error: event.error instanceof Error ? event.error.message : String(event.error),
|
||||
],
|
||||
invoke: [
|
||||
// 任务 1:加载配额(fromPromise,**返**结果 + onDone 设 flag)
|
||||
{
|
||||
id: "loadQuota",
|
||||
src: "loadQuota",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => {
|
||||
console.log("[chat-machine] guestSession.initializing.loadQuota.onDone", {
|
||||
remaining: event.output.remaining,
|
||||
total: event.output.total,
|
||||
});
|
||||
return {
|
||||
guestRemainingQuota: event.output.remaining,
|
||||
guestTotalQuota: event.output.total,
|
||||
quotaLoaded: true,
|
||||
};
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
// 失败**也**标 loaded,**不**卡 init(**游**客**可**能 quota 服务**不**可**用**,**让** UI **进** ready)
|
||||
actions: assign({
|
||||
quotaLoaded: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
// 任务 2:拉 history(fromPromise,**返**回**最**终** network messages**)
|
||||
{
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => {
|
||||
console.log(
|
||||
"[chat-machine] guestSession.initializing.loadHistory.onDone",
|
||||
{
|
||||
finalCount: event.output.messages.length,
|
||||
hasMore: event.output.hasMore,
|
||||
newOffset: event.output.newOffset,
|
||||
localOverwritten: event.output.localOverwritten,
|
||||
},
|
||||
);
|
||||
return {
|
||||
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: {
|
||||
@@ -267,17 +316,7 @@ export const chatMachine = setup({
|
||||
ChatSendImage: {
|
||||
actions: ["logSendImageReceived", "appendUserImage"],
|
||||
},
|
||||
ChatLoadMoreHistory: {
|
||||
actions: [
|
||||
"logLoadMoreHistoryReceived",
|
||||
({ context }) =>
|
||||
console.log("[chat-machine] guestSession.ready.ChatLoadMoreHistory received", {
|
||||
currentOffset: context.historyOffset,
|
||||
hasMore: context.hasMore,
|
||||
}),
|
||||
],
|
||||
target: "loadingMore",
|
||||
},
|
||||
// **删**除 ChatLoadMoreHistory handler —— 游客**无**服**务**端** history,**不**支**持**翻**页**
|
||||
ChatLogout: "#chat.idle",
|
||||
ChatNonGuestLogin: "#chat.userSession",
|
||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||
@@ -324,48 +363,18 @@ export const chatMachine = setup({
|
||||
},
|
||||
},
|
||||
},
|
||||
loadingMore: {
|
||||
entry: ({ context }) =>
|
||||
console.log("[chat-machine] → guestSession.loadingMore", { offset: context.historyOffset }),
|
||||
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,
|
||||
})),
|
||||
({ event }) =>
|
||||
console.log("[chat-machine] guestSession.loadingMore.onDone", {
|
||||
newMessagesCount: event.output.messages.length,
|
||||
newOffset: event.output.newOffset,
|
||||
hasMore: event.output.hasMore,
|
||||
}),
|
||||
],
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign({ isLoadingMore: false }),
|
||||
({ event }) =>
|
||||
console.error("[chat-machine] guestSession.loadingMore.onError", {
|
||||
error: event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// **删**除 loadingMore(游客**无**翻**页**)
|
||||
},
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// userSession(非游客会话 —— WS **父**级 + history **子**级**)
|
||||
// ========================================================================
|
||||
userSession: {
|
||||
initial: "initializing",
|
||||
exit: "clearWsConnected",
|
||||
invoke: {
|
||||
// 父级:WS 长**连**接(**一**进**来****就**连,跨 initializing/ready/sending/loadingMore)
|
||||
src: "chatWebSocket",
|
||||
input: ({ event }) => ({
|
||||
token: event.type === "ChatNonGuestLogin" ? event.token : "",
|
||||
@@ -373,35 +382,42 @@ export const chatMachine = setup({
|
||||
},
|
||||
states: {
|
||||
initializing: {
|
||||
invoke: {
|
||||
src: "loadMoreHistory",
|
||||
input: ({ context }) => ({ offset: context.historyOffset }),
|
||||
onDone: {
|
||||
// barrier:WS **连**上** + history **加**载**完**成**才**进** ready
|
||||
always: [
|
||||
{
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign(({ context, event }) => ({
|
||||
guard: ({ context }) => context.wsConnected && context.historyLoaded,
|
||||
},
|
||||
],
|
||||
invoke: {
|
||||
// 任务 2:拉 history(**只**调**用** loadHistoryActor,**不**再**用** loadMoreHistoryActor)
|
||||
src: "loadHistory",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => {
|
||||
console.log(
|
||||
"[chat-machine] userSession.initializing.loadHistory.onDone",
|
||||
{
|
||||
finalCount: event.output.messages.length,
|
||||
hasMore: event.output.hasMore,
|
||||
newOffset: event.output.newOffset,
|
||||
localOverwritten: event.output.localOverwritten,
|
||||
},
|
||||
);
|
||||
return {
|
||||
messages: event.output.messages,
|
||||
isLoadingMore: false,
|
||||
hasMore: event.output.hasMore,
|
||||
historyOffset: event.output.newOffset,
|
||||
})),
|
||||
({ event }) =>
|
||||
console.log("[chat-machine] userSession.initializing.onDone", {
|
||||
newMessagesCount: event.output.messages.length,
|
||||
newOffset: event.output.newOffset,
|
||||
hasMore: event.output.hasMore,
|
||||
}),
|
||||
],
|
||||
historyLoaded: true,
|
||||
};
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign({ isLoadingMore: false }),
|
||||
({ event }) =>
|
||||
console.error("[chat-machine] userSession.initializing.onError", {
|
||||
error: event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
],
|
||||
// 失败**也**标 loaded,**不**卡 init
|
||||
actions: assign({
|
||||
isLoadingMore: false,
|
||||
historyLoaded: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user