refactor(chat): export flow capabilities individually

This commit is contained in:
2026-07-06 15:13:19 +08:00
parent 99ac24e901
commit 3b53fc9bad
6 changed files with 245 additions and 197 deletions
+45 -50
View File
@@ -17,58 +17,53 @@ const log = new Logger("StoresChatChatHistoryFlow");
type UiMessage = import("@/data/dto/chat").UiMessage; type UiMessage = import("@/data/dto/chat").UiMessage;
export const chatHistoryActors = { export const loadHistoryActor = fromCallback<ChatEvent>(({ sendBack }) => {
loadHistory: fromCallback<ChatEvent>(({ sendBack }) => { let cancelled = false;
let cancelled = false;
void (async () => { void (async () => {
const localSnapshot = await readLocalHistorySnapshot(); const localSnapshot = await readLocalHistorySnapshot();
if (cancelled) return; if (cancelled) return;
sendBack({ sendBack({
type: "ChatLocalHistoryLoaded", type: "ChatLocalHistoryLoaded",
output: localSnapshot, output: localSnapshot,
});
const networkSnapshot = await syncNetworkHistory(
localSnapshot.localCount,
);
if (cancelled || !networkSnapshot) return;
sendBack({
type: "ChatNetworkHistoryLoaded",
output: networkSnapshot,
});
})().catch((error: unknown) => {
if (cancelled) return;
log.error("[chat-machine] loadHistoryActor failed", { error });
sendBack({ type: "ChatHistoryLoadFailed", error });
}); });
return () => { const networkSnapshot = await syncNetworkHistory(
cancelled = true; localSnapshot.localCount,
}; );
}), if (cancelled || !networkSnapshot) return;
sendBack({
type: "ChatNetworkHistoryLoaded",
output: networkSnapshot,
});
})().catch((error: unknown) => {
if (cancelled) return;
log.error("[chat-machine] loadHistoryActor failed", { error });
sendBack({ type: "ChatHistoryLoadFailed", error });
});
loadMoreHistory: fromPromise< return () => {
{ messages: UiMessage[]; hasMore: boolean; newOffset: number }, cancelled = true;
{ offset: number } };
>(async ({ input }) => { });
const chatRepo = getChatRepository();
const result = await chatRepo.getHistory(PAGE_SIZE, input.offset);
if (Result.isErr(result)) {
log.error("[chat-machine] loadMoreHistoryActor failed", {
error: result.error,
});
throw result.error;
}
const page = localMessagesToUi(result.data.messages);
void chatRepo.prefetchMediaForMessages(result.data.messages);
return {
messages: page,
hasMore: page.length >= PAGE_SIZE,
newOffset: input.offset + page.length,
};
}),
};
export const loadHistoryActor = chatHistoryActors.loadHistory; export const loadMoreHistoryActor = fromPromise<
export const loadMoreHistoryActor = chatHistoryActors.loadMoreHistory; { messages: UiMessage[]; hasMore: boolean; newOffset: number },
{ offset: number }
>(async ({ input }) => {
const chatRepo = getChatRepository();
const result = await chatRepo.getHistory(PAGE_SIZE, input.offset);
if (Result.isErr(result)) {
log.error("[chat-machine] loadMoreHistoryActor failed", {
error: result.error,
});
throw result.error;
}
const page = localMessagesToUi(result.data.messages);
void chatRepo.prefetchMediaForMessages(result.data.messages);
return {
messages: page,
hasMore: page.length >= PAGE_SIZE,
newOffset: input.offset + page.length,
};
});
-3
View File
@@ -7,17 +7,14 @@
* - chat-unlock-flow * - chat-unlock-flow
*/ */
export { export {
chatHistoryActors,
loadHistoryActor, loadHistoryActor,
loadMoreHistoryActor, loadMoreHistoryActor,
} from "./chat-history-flow"; } from "./chat-history-flow";
export { export {
chatSendActors,
httpMessageQueueActor, httpMessageQueueActor,
sendMessageHttpActor, sendMessageHttpActor,
} from "./chat-send-flow"; } from "./chat-send-flow";
export { export {
chatUnlockActors,
unlockHistoryActor, unlockHistoryActor,
unlockMessageActor, unlockMessageActor,
type UnlockMessageOutput, type UnlockMessageOutput,
+56 -10
View File
@@ -39,10 +39,38 @@ import {
shouldAutoUnlockHistory, shouldAutoUnlockHistory,
shouldPromptUnlockHistory, shouldPromptUnlockHistory,
} from "./chat-machine.helpers"; } from "./chat-machine.helpers";
import { chatHistoryActors } from "./chat-history-flow"; import {
import { chatMediaActions } from "./chat-media-flow"; loadHistoryActor,
import { chatSendActions, chatSendActors } from "./chat-send-flow"; loadMoreHistoryActor,
import { chatUnlockActions, chatUnlockActors } from "./chat-unlock-flow"; } from "./chat-history-flow";
import {
appendGuestUserImageAction,
appendUserImageAction,
} from "./chat-media-flow";
import {
appendGuestUserMessageAction,
appendQueuedSendErrorMessageAction,
appendUserMessageAction,
applyQueuedHttpOutputAction,
clearUpgradePromptAction,
httpMessageQueueActor,
markQueuedSendStartedAction,
sendMessageHttpActor,
} from "./chat-send-flow";
import {
applyUnlockMessageSucceededAction,
clearUnlockPaywallRequestAction,
dismissUnlockHistoryPromptAction,
markPaymentUnlockPendingAction,
markUnlockHistoryFailedAction,
markUnlockHistoryStartedAction,
markUnlockMessageStartedAction,
requestUnlockPaymentFromErrorAction,
requestUnlockPaymentFromOutputAction,
showUnlockHistoryPromptAction,
unlockHistoryActor,
unlockMessageActor,
} from "./chat-unlock-flow";
// 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变 // 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变
export type { ChatState } from "./chat-state"; export type { ChatState } from "./chat-state";
@@ -58,14 +86,32 @@ export const chatMachine = setup({
events: {} as ChatEvent, events: {} as ChatEvent,
}, },
actors: { actors: {
...chatHistoryActors, loadHistory: loadHistoryActor,
...chatSendActors, loadMoreHistory: loadMoreHistoryActor,
...chatUnlockActors, sendMessageHttp: sendMessageHttpActor,
httpMessageQueue: httpMessageQueueActor,
unlockHistory: unlockHistoryActor,
unlockMessage: unlockMessageActor,
}, },
actions: { actions: {
...chatSendActions, appendGuestUserMessage: appendGuestUserMessageAction,
...chatMediaActions, appendUserMessage: appendUserMessageAction,
...chatUnlockActions, appendQueuedSendErrorMessage: appendQueuedSendErrorMessageAction,
markQueuedSendStarted: markQueuedSendStartedAction,
applyQueuedHttpOutput: applyQueuedHttpOutputAction,
clearUpgradePrompt: clearUpgradePromptAction,
appendGuestUserImage: appendGuestUserImageAction,
appendUserImage: appendUserImageAction,
markPaymentUnlockPending: markPaymentUnlockPendingAction,
showUnlockHistoryPrompt: showUnlockHistoryPromptAction,
dismissUnlockHistoryPrompt: dismissUnlockHistoryPromptAction,
markUnlockHistoryStarted: markUnlockHistoryStartedAction,
markUnlockHistoryFailed: markUnlockHistoryFailedAction,
markUnlockMessageStarted: markUnlockMessageStartedAction,
applyUnlockMessageSucceeded: applyUnlockMessageSucceededAction,
requestUnlockPaymentFromOutput: requestUnlockPaymentFromOutputAction,
requestUnlockPaymentFromError: requestUnlockPaymentFromErrorAction,
clearUnlockPaywallRequest: clearUnlockPaywallRequestAction,
enqueueMessage: sendTo("messageQueue", ({ event }) => event), enqueueMessage: sendTo("messageQueue", ({ event }) => event),
startGuestSession: assign(() => ({ startGuestSession: assign(() => ({
+8 -6
View File
@@ -5,8 +5,8 @@ import { beginPendingReply } from "./chat-machine.helpers";
const log = new Logger("StoresChatChatMediaFlow"); const log = new Logger("StoresChatChatMediaFlow");
export const chatMediaActions = { export const appendGuestUserImageAction = chatAssign(
appendGuestUserImage: chatAssign(({ context, event }: ChatActionArgs) => { ({ context, event }: ChatActionArgs) => {
if (event.type !== "ChatSendImage") return {}; if (event.type !== "ChatSendImage") return {};
const today = todayString(); const today = todayString();
@@ -28,9 +28,11 @@ export const chatMediaActions = {
upgradePromptVisible: false, upgradePromptVisible: false,
upgradeReason: null, upgradeReason: null,
}; };
}), },
);
appendUserImage: chatAssign(({ context, event }: ChatActionArgs) => { export const appendUserImageAction = chatAssign(
({ context, event }: ChatActionArgs) => {
if (event.type !== "ChatSendImage") return {}; if (event.type !== "ChatSendImage") return {};
const today = todayString(); const today = todayString();
@@ -52,5 +54,5 @@ export const chatMediaActions = {
upgradePromptVisible: false, upgradePromptVisible: false,
upgradeReason: null, upgradeReason: null,
}; };
}), },
}; );
+37 -33
View File
@@ -23,21 +23,21 @@ const log = new Logger("StoresChatChatSendFlow");
type UiMessage = import("@/data/dto/chat").UiMessage; type UiMessage = import("@/data/dto/chat").UiMessage;
export const chatSendActors = { export const sendMessageHttpActor = fromPromise<
sendMessageHttp: fromPromise< { response: ChatSendResponse; reply: UiMessage | null },
{ response: ChatSendResponse; reply: UiMessage | null }, { content: string }
{ content: string } >(async ({ input }) => {
>(async ({ input }) => { return sendMessageViaHttp(input.content);
return sendMessageViaHttp(input.content); });
}),
httpMessageQueue: fromCallback<ChatEvent>(({ sendBack, receive }) => { export const httpMessageQueueActor = fromCallback<ChatEvent>(
({ sendBack, receive }) => {
return createMessageQueueActor(sendBack, receive); return createMessageQueueActor(sendBack, receive);
}), },
}; );
export const chatSendActions = { export const appendGuestUserMessageAction = chatAssign(
appendGuestUserMessage: chatAssign(({ context, event }: ChatActionArgs) => { ({ context, event }: ChatActionArgs) => {
if (event.type !== "ChatSendMessage") return {}; if (event.type !== "ChatSendMessage") return {};
const today = todayString(); const today = todayString();
@@ -60,9 +60,11 @@ export const chatSendActions = {
upgradePromptVisible: false, upgradePromptVisible: false,
upgradeReason: null, upgradeReason: null,
}; };
}), },
);
appendUserMessage: chatAssign(({ context, event }: ChatActionArgs) => { export const appendUserMessageAction = chatAssign(
({ context, event }: ChatActionArgs) => {
if (event.type !== "ChatSendMessage") return {}; if (event.type !== "ChatSendMessage") return {};
const today = todayString(); const today = todayString();
@@ -84,9 +86,11 @@ export const chatSendActions = {
upgradePromptVisible: false, upgradePromptVisible: false,
upgradeReason: null, upgradeReason: null,
}; };
}), },
);
appendQueuedSendErrorMessage: chatAssign(({ context }: ChatContextArgs) => { export const appendQueuedSendErrorMessageAction = chatAssign(
({ context }: ChatContextArgs) => {
const messages = [ const messages = [
...context.messages, ...context.messages,
{ {
@@ -96,29 +100,29 @@ export const chatSendActions = {
}, },
]; ];
return { messages, ...finishPendingReply(context) }; return { messages, ...finishPendingReply(context) };
}), },
);
markQueuedSendStarted: chatAssign(({ context }: ChatContextArgs) => export const markQueuedSendStartedAction = chatAssign(
({ context }: ChatContextArgs) =>
beginPendingReply(context), beginPendingReply(context),
), );
applyQueuedHttpOutput: chatAssign(({ context, event }: ChatActionArgs) => { export const applyQueuedHttpOutputAction = chatAssign(
({ context, event }: ChatActionArgs) => {
if (event.type !== "ChatQueuedHttpDone") return {}; if (event.type !== "ChatQueuedHttpDone") return {};
return applyHttpSendOutput(context, event.output); return applyHttpSendOutput(context, event.output);
}), },
);
clearUpgradePrompt: chatAssign(() => ({ export const clearUpgradePromptAction = chatAssign(() => ({
upgradePromptVisible: false, upgradePromptVisible: false,
upgradeReason: null, upgradeReason: null,
canSendMessage: true, canSendMessage: true,
cannotSendReason: null, cannotSendReason: null,
requiredCredits: 0, requiredCredits: 0,
shortfallCredits: 0, shortfallCredits: 0,
})), }));
};
export const sendMessageHttpActor = chatSendActors.sendMessageHttp;
export const httpMessageQueueActor = chatSendActors.httpMessageQueue;
function createMessageQueueActor( function createMessageQueueActor(
sendBack: (event: ChatEvent) => void, sendBack: (event: ChatEvent) => void,
+99 -95
View File
@@ -19,101 +19,102 @@ const log = new Logger("StoresChatChatUnlockFlow");
type UiMessage = import("@/data/dto/chat").UiMessage; type UiMessage = import("@/data/dto/chat").UiMessage;
export const chatUnlockActors = { export const unlockHistoryActor = fromPromise<{
unlockHistory: fromPromise<{ unlocked: boolean;
unlocked: boolean; reason: string;
reason: string; shortfallCredits: number;
shortfallCredits: number; messages: UiMessage[];
messages: UiMessage[]; hasMore: boolean;
hasMore: boolean; newOffset: number;
newOffset: number; }>(async () => {
}>(async () => { const chatRepo = getChatRepository();
const chatRepo = getChatRepository(); const unlockResult = await chatRepo.unlockHistory();
const unlockResult = await chatRepo.unlockHistory(); if (Result.isErr(unlockResult)) {
if (Result.isErr(unlockResult)) { log.error("[chat-machine] unlockHistoryActor failed", {
log.error("[chat-machine] unlockHistoryActor failed", { error: unlockResult.error,
error: unlockResult.error, });
}); throw unlockResult.error;
throw unlockResult.error; }
}
const history = await readAndSyncHistory(); const history = await readAndSyncHistory();
return { return {
unlocked: unlockResult.data.unlocked, unlocked: unlockResult.data.unlocked,
reason: unlockResult.data.reason, reason: unlockResult.data.reason,
shortfallCredits: unlockResult.data.shortfallCredits, shortfallCredits: unlockResult.data.shortfallCredits,
messages: history.messages, messages: history.messages,
hasMore: history.hasMore, hasMore: history.hasMore,
newOffset: history.newOffset, newOffset: history.newOffset,
}; };
}), });
unlockMessage: fromPromise<UnlockMessageOutput, { messageId: string }>( export const unlockMessageActor = fromPromise<
async ({ input }) => { UnlockMessageOutput,
const chatRepo = getChatRepository(); { messageId: string }
const unlockResult = await chatRepo.unlockPrivateMessage(input.messageId); >(async ({ input }) => {
if (Result.isErr(unlockResult)) { const chatRepo = getChatRepository();
log.error("[chat-machine] unlockMessageActor failed", { const unlockResult = await chatRepo.unlockPrivateMessage(input.messageId);
messageId: input.messageId, if (Result.isErr(unlockResult)) {
error: unlockResult.error, log.error("[chat-machine] unlockMessageActor failed", {
}); messageId: input.messageId,
throw unlockResult.error; error: unlockResult.error,
} });
throw unlockResult.error;
}
if (unlockResult.data.unlocked) { if (unlockResult.data.unlocked) {
const markResult = await chatRepo.markPrivateMessageUnlockedInLocal( const markResult = await chatRepo.markPrivateMessageUnlockedInLocal(
input.messageId, input.messageId,
unlockResult.data.lockDetail, unlockResult.data.lockDetail,
); );
if (Result.isErr(markResult)) { if (Result.isErr(markResult)) {
log.warn("[chat-machine] mark unlocked local message failed", { log.warn("[chat-machine] mark unlocked local message failed", {
messageId: input.messageId,
error: markResult.error,
});
}
}
return {
messageId: input.messageId, messageId: input.messageId,
response: unlockResult.data, error: markResult.error,
}; });
}, }
), }
};
export const chatUnlockActions = { return {
markPaymentUnlockPending: chatAssign(() => ({ messageId: input.messageId,
paymentUnlockPending: true, response: unlockResult.data,
unlockHistoryError: null, };
})), });
showUnlockHistoryPrompt: chatAssign(({ context }: ChatContextArgs) => ({ export const markPaymentUnlockPendingAction = chatAssign(() => ({
paymentUnlockPending: true,
unlockHistoryError: null,
}));
export const showUnlockHistoryPromptAction = chatAssign(
({ context }: ChatContextArgs) => ({
paymentUnlockPending: false, paymentUnlockPending: false,
unlockHistoryPromptVisible: true, unlockHistoryPromptVisible: true,
lockedHistoryCount: countLockedHistoryMessages(context.messages), lockedHistoryCount: countLockedHistoryMessages(context.messages),
unlockHistoryError: null, unlockHistoryError: null,
})), }),
);
dismissUnlockHistoryPrompt: chatAssign(() => ({ export const dismissUnlockHistoryPromptAction = chatAssign(() => ({
paymentUnlockPending: false, paymentUnlockPending: false,
unlockHistoryPromptVisible: false, unlockHistoryPromptVisible: false,
unlockHistoryError: null, unlockHistoryError: null,
})), }));
markUnlockHistoryStarted: chatAssign(() => ({ export const markUnlockHistoryStartedAction = chatAssign(() => ({
paymentUnlockPending: false, paymentUnlockPending: false,
unlockHistoryPromptVisible: false, unlockHistoryPromptVisible: false,
isUnlockingHistory: true, isUnlockingHistory: true,
unlockHistoryError: null, unlockHistoryError: null,
})), }));
markUnlockHistoryFailed: chatAssign(() => ({ export const markUnlockHistoryFailedAction = chatAssign(() => ({
isUnlockingHistory: false, isUnlockingHistory: false,
unlockHistoryPromptVisible: true, unlockHistoryPromptVisible: true,
unlockHistoryError: "Failed to unlock messages. Please try again.", unlockHistoryError: "Failed to unlock messages. Please try again.",
})), }));
markUnlockMessageStarted: chatAssign(({ event }: ChatActionArgs) => { export const markUnlockMessageStartedAction = chatAssign(
({ event }: ChatActionArgs) => {
if (event.type !== "ChatUnlockMessageRequested") return {}; if (event.type !== "ChatUnlockMessageRequested") return {};
return { return {
isUnlockingMessage: true, isUnlockingMessage: true,
@@ -122,9 +123,11 @@ export const chatUnlockActions = {
unlockMessageError: null, unlockMessageError: null,
unlockPaywallRequest: null, unlockPaywallRequest: null,
}; };
}), },
);
applyUnlockMessageSucceeded: chatAssign(({ context, event }: ChatActionArgs) => { export const applyUnlockMessageSucceededAction = chatAssign(
({ context, event }: ChatActionArgs) => {
const output = (event as { output?: UnlockMessageOutput }).output; const output = (event as { output?: UnlockMessageOutput }).output;
if (!output) return {}; if (!output) return {};
return { return {
@@ -139,9 +142,11 @@ export const chatUnlockActions = {
requiredCredits: 0, requiredCredits: 0,
shortfallCredits: 0, shortfallCredits: 0,
}; };
}), },
);
requestUnlockPaymentFromOutput: chatAssign(({ context, event }: ChatActionArgs) => { export const requestUnlockPaymentFromOutputAction = chatAssign(
({ context, event }: ChatActionArgs) => {
const output = (event as { output?: UnlockMessageOutput }).output; const output = (event as { output?: UnlockMessageOutput }).output;
if (!output) return {}; if (!output) return {};
return { return {
@@ -161,9 +166,11 @@ export const chatUnlockActions = {
requiredCredits: output.response.requiredCredits, requiredCredits: output.response.requiredCredits,
shortfallCredits: output.response.shortfallCredits, shortfallCredits: output.response.shortfallCredits,
}; };
}), },
);
requestUnlockPaymentFromError: chatAssign(({ context }: ChatContextArgs) => ({ export const requestUnlockPaymentFromErrorAction = chatAssign(
({ context }: ChatContextArgs) => ({
isUnlockingMessage: false, isUnlockingMessage: false,
unlockMessageError: "unlock_failed", unlockMessageError: "unlock_failed",
unlockPaywallRequest: unlockPaywallRequest:
@@ -179,13 +186,10 @@ export const chatUnlockActions = {
: null, : null,
unlockingMessageId: null, unlockingMessageId: null,
unlockingMessageKind: null, unlockingMessageKind: null,
})), }),
);
clearUnlockPaywallRequest: chatAssign(() => ({ export const clearUnlockPaywallRequestAction = chatAssign(() => ({
unlockPaywallRequest: null, unlockPaywallRequest: null,
})), }));
};
export const unlockHistoryActor = chatUnlockActors.unlockHistory;
export const unlockMessageActor = chatUnlockActors.unlockMessage;
export type { UnlockMessageOutput } from "./chat-machine.helpers"; export type { UnlockMessageOutput } from "./chat-machine.helpers";