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"段:
* - `chatInitActor`fromPromise**仅**游客有意义)
* - `sendMessageHttpActor`fromPromise**已定义但** wire —— 消息**不**真发后端
* - `sendMessageHttpActor`fromPromise**已** wire —— "一次发送 = 一次返回"
* - `loadMoreHistoryActor`fromPromise
* - `chatWebSocketActor`fromCallback,长生命周期 —— **仅** userSession 期间 invoke
*
@@ -24,6 +24,7 @@ import {
chatRepo,
localMessagesToUi,
readInitData,
sendResponseToUiMessage,
type InitResult,
} from "./chat-machine.helpers";
import type { ChatEvent } from "./chat-events";
@@ -34,12 +35,13 @@ import type { ChatEvent } from "./chat-events";
export const chatInitActor = fromPromise<InitResult>(async () => readInitData());
/**
* HTTP 发送消息(**已定义但未** wire —— 消息**不**真发后端
* - 任何 state 转移**** invoke 此 actor
* - 保留全链日志以便排查
* HTTP 发送消息(**一次发送 = 一次返回**
* - 后端响应****是 AI 回复(`ChatSendResponse.reply: string`
* - **不**再调 `getLocalMessages()`**多此一举**
* - 返回 `{ reply: UiMessage }` —— `sending.onDone` 把它**追加**到 `context.messages`
*/
export const sendMessageHttpActor = fromPromise<
{ messages: UiMessage[] },
{ reply: UiMessage },
{ content: string }
>(async ({ input, self }) => {
console.log("[chat-machine] sendMessageHttpActor ENTRY", {
@@ -48,10 +50,6 @@ export const sendMessageHttpActor = fromPromise<
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", {
@@ -63,18 +61,18 @@ export const sendMessageHttpActor = fromPromise<
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,
// 一次发送 = 一次返回 —— 直接转 reply → UiMessage
console.log("[chat-machine] sendMessageHttpActor converting reply to UiMessage", {
replyLength: result.data.reply.length,
replyPreview: result.data.reply.slice(0, 50),
messageId: result.data.messageId,
timestamp: result.data.timestamp,
});
if (Result.isOk(local) && local.data) {
const messages = localMessagesToUi(local.data);
console.log("[chat-machine] sendMessageHttpActor done", { messagesCount: messages.length });
return { messages };
}
return { messages: [] };
const reply = sendResponseToUiMessage(result.data);
console.log("[chat-machine] sendMessageHttpActor done", {
replyContentLength: reply.content.length,
});
return { reply };
});
/** 翻历史(pagination */
+15
View File
@@ -19,6 +19,8 @@ import { chatRepository } from "@/data/repositories/chat_repository";
import type { IChatRepository } from "@/data/repositories/interfaces";
import { ChatStorage } from "@/data/storage/chat/chat_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";
// ============================================================// 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**纯函数** */
+84 -10
View File
@@ -1,13 +1,6 @@
/**
* 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
* - HTTP 操作用 `fromPromise` actor(一次性 Promise
@@ -27,15 +20,17 @@
* - `guestSession`**parent**):游客会话 —— **不** invoke WS
* - `initializing`invoke chatInit(拉本地 + 初始化配额)
* - `ready`:用户聊天 + 翻历史
* - `sending`invoke sendMessageHttpActorHTTP 发送消息)
* - `loadingMore`invoke loadMoreHistory(翻历史)
* - `userSession`**parent**):非游客会话 —— **invoke WS**(持久)
* - `initializing`invoke loadMoreHistory(拉服务器端首屏)
* - `ready`:用户聊天 + 翻历史
* - `sending`invoke sendMessageHttpActorHTTP fallback —— WS 真发**未** wire
* - `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
* - `ChatWebSocketConnected` 事件 → `wsConnected = true`
* - `userSession.exit` action → `wsConnected = false`WS actor cleanup 时**也**会跑 exit
@@ -279,6 +274,7 @@ export const chatMachine = setup({
ChatSendMessage: {
actions: ["logSendMessageReceived", "appendUserMessage"],
guard: ({ event }) => event.content.trim().length > 0,
target: "sending",
},
ChatSendImage: {
actions: ["logSendImageReceived", "appendUserImage"],
@@ -302,6 +298,44 @@ export const chatMachine = setup({
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: {
entry: ({ context }) =>
console.log("[chat-machine] → guestSession.loadingMore", { offset: context.historyOffset }),
@@ -357,7 +391,7 @@ export const chatMachine = setup({
onDone: {
target: "ready",
actions: [
assign(({ event }) => ({
assign(({ context, event }) => ({
messages: event.output.messages,
isLoadingMore: false,
hasMore: event.output.hasMore,
@@ -388,6 +422,7 @@ export const chatMachine = setup({
ChatSendMessage: {
actions: ["logSendMessageReceived", "appendUserMessage"],
guard: ({ event }) => event.content.trim().length > 0,
target: "sending",
},
ChatSendImage: {
actions: ["logSendImageReceived", "appendUserImage"],
@@ -411,6 +446,45 @@ export const chatMachine = setup({
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: {
entry: ({ context }) =>
console.log("[chat-machine] → userSession.loadingMore", { offset: context.historyOffset }),