refactor(chat): wire sendMessageHttpActor for HTTP message sending and update response handling

This commit is contained in:
2026-06-12 11:45:46 +08:00
parent 38f060bbd8
commit a9938b8dae
3 changed files with 117 additions and 30 deletions
+18 -20
View File
@@ -3,7 +3,7 @@
* *
* 从 `chat-machine.ts` 抽出的"Actors"段: * 从 `chat-machine.ts` 抽出的"Actors"段:
* - `chatInitActor`fromPromise**仅**游客有意义) * - `chatInitActor`fromPromise**仅**游客有意义)
* - `sendMessageHttpActor`fromPromise**已定义但** wire —— 消息**不**真发后端 * - `sendMessageHttpActor`fromPromise**已** wire —— "一次发送 = 一次返回"
* - `loadMoreHistoryActor`fromPromise * - `loadMoreHistoryActor`fromPromise
* - `chatWebSocketActor`fromCallback,长生命周期 —— **仅** userSession 期间 invoke * - `chatWebSocketActor`fromCallback,长生命周期 —— **仅** userSession 期间 invoke
* *
@@ -24,6 +24,7 @@ import {
chatRepo, chatRepo,
localMessagesToUi, localMessagesToUi,
readInitData, readInitData,
sendResponseToUiMessage,
type InitResult, type InitResult,
} from "./chat-machine.helpers"; } from "./chat-machine.helpers";
import type { ChatEvent } from "./chat-events"; import type { ChatEvent } from "./chat-events";
@@ -34,12 +35,13 @@ import type { ChatEvent } from "./chat-events";
export const chatInitActor = fromPromise<InitResult>(async () => readInitData()); export const chatInitActor = fromPromise<InitResult>(async () => readInitData());
/** /**
* HTTP 发送消息(**已定义但未** wire —— 消息**不**真发后端 * HTTP 发送消息(**一次发送 = 一次返回**
* - 任何 state 转移**** invoke 此 actor * - 后端响应****是 AI 回复(`ChatSendResponse.reply: string`
* - 保留全链日志以便排查 * - **不**再调 `getLocalMessages()`**多此一举**
* - 返回 `{ reply: UiMessage }` —— `sending.onDone` 把它**追加**到 `context.messages`
*/ */
export const sendMessageHttpActor = fromPromise< export const sendMessageHttpActor = fromPromise<
{ messages: UiMessage[] }, { reply: UiMessage },
{ content: string } { content: string }
>(async ({ input, self }) => { >(async ({ input, self }) => {
console.log("[chat-machine] sendMessageHttpActor ENTRY", { console.log("[chat-machine] sendMessageHttpActor ENTRY", {
@@ -48,10 +50,6 @@ export const sendMessageHttpActor = fromPromise<
selfId: self.id, selfId: self.id,
selfPath: "<actor path>", 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"); console.log("[chat-machine] sendMessageHttpActor calling chatRepo.sendMessage");
const result = await chatRepo.sendMessage(input.content); const result = await chatRepo.sendMessage(input.content);
console.log("[chat-machine] sendMessageHttpActor chatRepo.sendMessage DONE", { console.log("[chat-machine] sendMessageHttpActor chatRepo.sendMessage DONE", {
@@ -63,18 +61,18 @@ export const sendMessageHttpActor = fromPromise<
throw result.error; throw result.error;
} }
console.log("[chat-machine] sendMessageHttpActor fetching local messages"); // 一次发送 = 一次返回 —— 直接转 reply → UiMessage
const local = await chatRepo.getLocalMessages(); console.log("[chat-machine] sendMessageHttpActor converting reply to UiMessage", {
console.log("[chat-machine] sendMessageHttpActor local messages DONE", { replyLength: result.data.reply.length,
success: local.success, replyPreview: result.data.reply.slice(0, 50),
messagesCount: local.success && local.data ? local.data.length : 0, messageId: result.data.messageId,
timestamp: result.data.timestamp,
}); });
if (Result.isOk(local) && local.data) { const reply = sendResponseToUiMessage(result.data);
const messages = localMessagesToUi(local.data); console.log("[chat-machine] sendMessageHttpActor done", {
console.log("[chat-machine] sendMessageHttpActor done", { messagesCount: messages.length }); replyContentLength: reply.content.length,
return { messages }; });
} return { reply };
return { messages: [] };
}); });
/** 翻历史(pagination */ /** 翻历史(pagination */
+15
View File
@@ -19,6 +19,8 @@ import { chatRepository } from "@/data/repositories/chat_repository";
import type { IChatRepository } from "@/data/repositories/interfaces"; import type { IChatRepository } from "@/data/repositories/interfaces";
import { ChatStorage } from "@/data/storage/chat/chat_storage"; import { ChatStorage } from "@/data/storage/chat/chat_storage";
import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
import { formatDate } from "@/utils/date";
import { Result } from "@/utils/result"; import { Result } from "@/utils/result";
// ============================================================// Constants // ============================================================// Constants
@@ -52,6 +54,19 @@ export function localMessagesToUi(
})); }));
} }
/**
* ChatSendResponse → UiMessage**纯函数**
* - 业务事实:后端响应**就是** AI 的回复
* - 用后端 `timestamp`**不**用本地 time)—— 多设备/时区**一致**
*/
export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
return {
content: response.reply,
isFromAI: true,
date: formatDate(new Date(response.timestamp)),
};
}
// ============================================================// Result → number 映射 // ============================================================// Result → number 映射
// ============================================================ // ============================================================
/** 日配额 Result → number**纯函数** */ /** 日配额 Result → number**纯函数** */
+84 -10
View File
@@ -1,13 +1,6 @@
/** /**
* Chat 状态机(XState v5 * Chat 状态机(XState v5
* *
* 原始 Dart: lib/ui/chat/bloc/chat bloc + chat state + chat event
*
* 本轮迁移范围:
* ✅ HTTP 流程(init / send message / load more history
* ✅ WebSocket 长生命周期(**由** chat 机器内 fromCallback actor 管)
* ✅ 上下文数据(messages / quota / 标志位)
*
* 设计要点: * 设计要点:
* - 使用 XState v5 `setup({...}).createMachine({...})` 声明式 API * - 使用 XState v5 `setup({...}).createMachine({...})` 声明式 API
* - HTTP 操作用 `fromPromise` actor(一次性 Promise * - HTTP 操作用 `fromPromise` actor(一次性 Promise
@@ -27,15 +20,17 @@
* - `guestSession`**parent**):游客会话 —— **不** invoke WS * - `guestSession`**parent**):游客会话 —— **不** invoke WS
* - `initializing`invoke chatInit(拉本地 + 初始化配额) * - `initializing`invoke chatInit(拉本地 + 初始化配额)
* - `ready`:用户聊天 + 翻历史 * - `ready`:用户聊天 + 翻历史
* - `sending`invoke sendMessageHttpActorHTTP 发送消息)
* - `loadingMore`invoke loadMoreHistory(翻历史) * - `loadingMore`invoke loadMoreHistory(翻历史)
* - `userSession`**parent**):非游客会话 —— **invoke WS**(持久) * - `userSession`**parent**):非游客会话 —— **invoke WS**(持久)
* - `initializing`invoke loadMoreHistory(拉服务器端首屏) * - `initializing`invoke loadMoreHistory(拉服务器端首屏)
* - `ready`:用户聊天 + 翻历史 * - `ready`:用户聊天 + 翻历史
* - `sending`invoke sendMessageHttpActorHTTP fallback —— WS 真发**未** wire
* - `loadingMore`invoke loadMoreHistory(翻历史) * - `loadingMore`invoke loadMoreHistory(翻历史)
* WS 在 `userSession` parent 状态 invoke —— 跨 initializing/ready/loadingMore 持续 * WS 在 `userSession` parent 状态 invoke —— 跨 initializing/ready/sending/loadingMore 持续
* *
* **消息发送路径**(**业务事实**): * **消息发送路径**(**业务事实**):
* - `wsConnected: true`**仅** userSession 期间)→ **走 WS****不**消耗次数 * - `wsConnected: true`**仅** userSession 期间)→ **走 WS****不**消耗次数**仅**配额规则)
* - `wsConnected: false` → **走 HTTP**,消耗次数(**仅**游客有意义 —— 非游客 0-1=max(0,-1)=0 天然 no-op * - `wsConnected: false` → **走 HTTP**,消耗次数(**仅**游客有意义 —— 非游客 0-1=max(0,-1)=0 天然 no-op
* - `ChatWebSocketConnected` 事件 → `wsConnected = true` * - `ChatWebSocketConnected` 事件 → `wsConnected = true`
* - `userSession.exit` action → `wsConnected = false`WS actor cleanup 时**也**会跑 exit * - `userSession.exit` action → `wsConnected = false`WS actor cleanup 时**也**会跑 exit
@@ -279,6 +274,7 @@ export const chatMachine = setup({
ChatSendMessage: { ChatSendMessage: {
actions: ["logSendMessageReceived", "appendUserMessage"], actions: ["logSendMessageReceived", "appendUserMessage"],
guard: ({ event }) => event.content.trim().length > 0, guard: ({ event }) => event.content.trim().length > 0,
target: "sending",
}, },
ChatSendImage: { ChatSendImage: {
actions: ["logSendImageReceived", "appendUserImage"], actions: ["logSendImageReceived", "appendUserImage"],
@@ -302,6 +298,44 @@ export const chatMachine = setup({
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" }, ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
}, },
}, },
sending: {
entry: ({ event }) =>
console.log("[chat-machine] → guestSession.sending", {
contentLength: event.type === "ChatSendMessage" ? event.content.length : 0,
contentPreview: event.type === "ChatSendMessage" ? event.content.slice(0, 50) : "",
}),
invoke: {
src: "sendMessageHttp",
input: ({ event }) => ({
content: event.type === "ChatSendMessage" ? event.content : "",
}),
onDone: {
target: "ready",
actions: [
assign(({ context, event }) => ({
messages: [...context.messages, event.output.reply],
isReplyingAI: false,
})),
({ context, event }) =>
console.log("[chat-machine] guestSession.sending.onDone", {
newReplyContentLength: event.output.reply.content.length,
totalMessagesCount: context.messages.length + 1,
isReplyingAI: false,
}),
],
},
onError: {
target: "ready",
actions: [
assign({ isReplyingAI: false }),
({ event }) =>
console.error("[chat-machine] guestSession.sending.onError", {
error: event.error instanceof Error ? event.error.message : String(event.error),
}),
],
},
},
},
loadingMore: { loadingMore: {
entry: ({ context }) => entry: ({ context }) =>
console.log("[chat-machine] → guestSession.loadingMore", { offset: context.historyOffset }), console.log("[chat-machine] → guestSession.loadingMore", { offset: context.historyOffset }),
@@ -357,7 +391,7 @@ export const chatMachine = setup({
onDone: { onDone: {
target: "ready", target: "ready",
actions: [ actions: [
assign(({ event }) => ({ assign(({ context, event }) => ({
messages: event.output.messages, messages: event.output.messages,
isLoadingMore: false, isLoadingMore: false,
hasMore: event.output.hasMore, hasMore: event.output.hasMore,
@@ -388,6 +422,7 @@ export const chatMachine = setup({
ChatSendMessage: { ChatSendMessage: {
actions: ["logSendMessageReceived", "appendUserMessage"], actions: ["logSendMessageReceived", "appendUserMessage"],
guard: ({ event }) => event.content.trim().length > 0, guard: ({ event }) => event.content.trim().length > 0,
target: "sending",
}, },
ChatSendImage: { ChatSendImage: {
actions: ["logSendImageReceived", "appendUserImage"], actions: ["logSendImageReceived", "appendUserImage"],
@@ -411,6 +446,45 @@ export const chatMachine = setup({
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" }, ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
}, },
}, },
sending: {
entry: ({ event }) =>
console.log("[chat-machine] → userSession.sending", {
contentLength: event.type === "ChatSendMessage" ? event.content.length : 0,
contentPreview: event.type === "ChatSendMessage" ? event.content.slice(0, 50) : "",
wsConnected: true,
}),
invoke: {
src: "sendMessageHttp",
input: ({ event }) => ({
content: event.type === "ChatSendMessage" ? event.content : "",
}),
onDone: {
target: "ready",
actions: [
assign(({ context, event }) => ({
messages: [...context.messages, event.output.reply],
isReplyingAI: false,
})),
({ context, event }) =>
console.log("[chat-machine] userSession.sending.onDone", {
newReplyContentLength: event.output.reply.content.length,
totalMessagesCount: context.messages.length + 1,
isReplyingAI: false,
}),
],
},
onError: {
target: "ready",
actions: [
assign({ isReplyingAI: false }),
({ event }) =>
console.error("[chat-machine] userSession.sending.onError", {
error: event.error instanceof Error ? event.error.message : String(event.error),
}),
],
},
},
},
loadingMore: { loadingMore: {
entry: ({ context }) => entry: ({ context }) =>
console.log("[chat-machine] → userSession.loadingMore", { offset: context.historyOffset }), console.log("[chat-machine] → userSession.loadingMore", { offset: context.historyOffset }),