refactor(chat): wire sendMessageHttpActor for HTTP message sending and update response handling
This commit is contained in:
@@ -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) */
|
||||||
|
|||||||
@@ -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(**纯函数**) */
|
||||||
|
|||||||
@@ -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 sendMessageHttpActor(HTTP 发送消息)
|
||||||
* - `loadingMore`:invoke loadMoreHistory(翻历史)
|
* - `loadingMore`:invoke loadMoreHistory(翻历史)
|
||||||
* - `userSession`(**parent**):非游客会话 —— **invoke WS**(持久)
|
* - `userSession`(**parent**):非游客会话 —— **invoke WS**(持久)
|
||||||
* - `initializing`:invoke loadMoreHistory(拉服务器端首屏)
|
* - `initializing`:invoke loadMoreHistory(拉服务器端首屏)
|
||||||
* - `ready`:用户聊天 + 翻历史
|
* - `ready`:用户聊天 + 翻历史
|
||||||
|
* - `sending`:invoke sendMessageHttpActor(HTTP 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 }),
|
||||||
|
|||||||
Reference in New Issue
Block a user