refactor(chat): split machine and repository flows

This commit is contained in:
2026-06-30 19:16:33 +08:00
parent d33c34d751
commit ed109f25a0
12 changed files with 908 additions and 730 deletions
+10 -250
View File
@@ -29,31 +29,19 @@
import { setup, assign, sendTo } from "xstate";
import { todayString, Logger } from "@/utils";
import { ChatState, initialState } from "./chat-state";
import type { ChatEvent } from "./chat-events";
import {
applyHttpSendOutput,
applyHistoryLoadedOutput,
applySingleUnlockOutput,
beginPendingReply,
countLockedHistoryMessages,
finishPendingReply,
shouldAutoUnlockHistory,
shouldPromptUnlockHistory,
} from "./chat-machine.helpers";
import {
loadHistoryActor,
sendMessageHttpActor,
loadMoreHistoryActor,
httpMessageQueueActor,
unlockHistoryActor,
unlockMessageActor,
type UnlockMessageOutput,
} from "./chat-machine.actors";
const log = new Logger("StoresChatChatMachine");
import { chatHistoryActors } from "./chat-history-flow";
import { chatMediaActions } from "./chat-media-flow";
import { chatSendActions, chatSendActors } from "./chat-send-flow";
import { chatUnlockActions, chatUnlockActors } from "./chat-unlock-flow";
// 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变
export type { ChatState } from "./chat-state";
@@ -69,14 +57,14 @@ export const chatMachine = setup({
events: {} as ChatEvent,
},
actors: {
loadHistory: loadHistoryActor,
sendMessageHttp: sendMessageHttpActor,
loadMoreHistory: loadMoreHistoryActor,
httpMessageQueue: httpMessageQueueActor,
unlockHistory: unlockHistoryActor,
unlockMessage: unlockMessageActor,
...chatHistoryActors,
...chatSendActors,
...chatUnlockActors,
},
actions: {
...chatSendActions,
...chatMediaActions,
...chatUnlockActions,
enqueueMessage: sendTo("messageQueue", ({ event }) => event),
startGuestSession: assign(() => ({
@@ -90,234 +78,6 @@ export const chatMachine = setup({
clearChatSession: assign(() => ({
...initialState,
})),
appendGuestUserMessage: assign(({ context, event }) => {
if (event.type !== "ChatSendMessage") return {};
const today = todayString();
log.debug("[chat-machine] appendGuestUserMessage", {
contentLength: event.content.length,
contentPreview: event.content.slice(0, 50),
quotaMode: "server controlled",
isReplyingAI: context.isReplyingAI,
});
return {
messages: [
...context.messages,
{
content: event.content,
isFromAI: false,
date: today,
},
],
upgradePromptVisible: false,
upgradeReason: null,
};
}),
appendUserMessage: assign(({ context, event }) => {
if (event.type !== "ChatSendMessage") return {};
const today = todayString();
log.debug("[chat-machine] appendUserMessage", {
contentLength: event.content.length,
contentPreview: event.content.slice(0, 50),
isReplyingAI: context.isReplyingAI,
});
return {
messages: [
...context.messages,
{
content: event.content,
isFromAI: false,
date: today,
},
],
upgradePromptVisible: false,
upgradeReason: null,
};
}),
appendGuestUserImage: assign(({ context, event }) => {
if (event.type !== "ChatSendImage") return {};
const today = todayString();
log.debug("[chat-machine] appendGuestUserImage", {
oldMessagesCount: context.messages.length,
quotaMode: "server controlled",
});
return {
messages: [
...context.messages,
{
content: "[Image]",
isFromAI: false,
date: today,
imageUrl: event.imageBase64,
},
],
...beginPendingReply(context),
upgradePromptVisible: false,
upgradeReason: null,
};
}),
appendUserImage: assign(({ context, event }) => {
if (event.type !== "ChatSendImage") return {};
const today = todayString();
log.debug("[chat-machine] appendUserImage", {
oldMessagesCount: context.messages.length,
});
return {
messages: [
...context.messages,
{
content: "[Image]",
isFromAI: false,
date: today,
imageUrl: event.imageBase64,
},
],
...beginPendingReply(context),
upgradePromptVisible: false,
upgradeReason: null,
};
}),
appendQueuedSendErrorMessage: assign(({ context }) => {
const messages = [
...context.messages,
{
content: "Something went wrong. Try sending again?",
isFromAI: true,
date: todayString(),
},
];
return { messages, ...finishPendingReply(context) };
}),
markQueuedSendStarted: assign(({ context }) => beginPendingReply(context)),
applyQueuedHttpOutput: assign(({ context, event }) => {
if (event.type !== "ChatQueuedHttpDone") return {};
return applyHttpSendOutput(context, event.output);
}),
clearUpgradePrompt: assign(() => ({
upgradePromptVisible: false,
upgradeReason: null,
canSendMessage: true,
cannotSendReason: null,
requiredCredits: 0,
shortfallCredits: 0,
})),
markPaymentUnlockPending: assign(() => ({
paymentUnlockPending: true,
unlockHistoryError: null,
})),
showUnlockHistoryPrompt: assign(({ context }) => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: true,
lockedHistoryCount: countLockedHistoryMessages(context.messages),
unlockHistoryError: null,
})),
dismissUnlockHistoryPrompt: assign(() => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: false,
unlockHistoryError: null,
})),
markUnlockHistoryStarted: assign(() => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: false,
isUnlockingHistory: true,
unlockHistoryError: null,
})),
markUnlockHistoryFailed: assign(() => ({
isUnlockingHistory: false,
unlockHistoryPromptVisible: true,
unlockHistoryError: "Failed to unlock messages. Please try again.",
})),
markUnlockMessageStarted: assign(({ event }) => {
if (event.type !== "ChatUnlockMessageRequested") return {};
return {
isUnlockingMessage: true,
unlockingMessageId: event.messageId,
unlockingMessageKind: event.kind,
unlockMessageError: null,
unlockPaywallRequest: null,
};
}),
applyUnlockMessageSucceeded: assign(({ context, event }) => {
const output = (event as { output?: UnlockMessageOutput }).output;
if (!output) return {};
return {
messages: applySingleUnlockOutput(context.messages, output),
isUnlockingMessage: false,
unlockingMessageId: null,
unlockingMessageKind: null,
unlockMessageError: null,
unlockPaywallRequest: null,
creditBalance: output.response.creditBalance,
creditsCharged: output.response.creditsCharged,
requiredCredits: 0,
shortfallCredits: 0,
};
}),
requestUnlockPaymentFromOutput: assign(({ context, event }) => {
const output = (event as { output?: UnlockMessageOutput }).output;
if (!output) return {};
return {
isUnlockingMessage: false,
unlockingMessageId: null,
unlockingMessageKind: null,
unlockMessageError: output.response.reason,
unlockPaywallRequest: {
messageId: output.messageId,
kind: context.unlockingMessageKind ?? "private",
reason: output.response.reason,
creditBalance: output.response.creditBalance,
requiredCredits: output.response.requiredCredits,
shortfallCredits: output.response.shortfallCredits,
},
creditBalance: output.response.creditBalance,
requiredCredits: output.response.requiredCredits,
shortfallCredits: output.response.shortfallCredits,
};
}),
requestUnlockPaymentFromError: assign(({ context }) => ({
isUnlockingMessage: false,
unlockMessageError: "unlock_failed",
unlockPaywallRequest:
context.unlockingMessageId && context.unlockingMessageKind
? {
messageId: context.unlockingMessageId,
kind: context.unlockingMessageKind,
reason: "unlock_failed",
creditBalance: context.creditBalance,
requiredCredits: context.requiredCredits,
shortfallCredits: context.shortfallCredits,
}
: null,
unlockingMessageId: null,
unlockingMessageKind: null,
})),
clearUnlockPaywallRequest: assign(() => ({
unlockPaywallRequest: null,
})),
},
}).createMachine({
id: "chat",