feat(chat): sync multi-role backend APIs

This commit is contained in:
2026-07-20 11:29:54 +08:00
parent 16b5c16e76
commit b6fdc912ae
84 changed files with 1488 additions and 439 deletions
+4
View File
@@ -8,6 +8,7 @@ import { resolveChatConversationKey } from "@/data/repositories/chat_cache_ident
import { Logger } from "@/utils/logger";
import { Result } from "@/utils/result";
import { isAbortError } from "@/utils/abort";
import { getCharacterErrorCode } from "@/data/services/api";
import type { ChatEvent } from "../../chat-events";
import { sendResponseToUiMessage } from "../../helper/message-mappers";
@@ -61,6 +62,9 @@ function createMessageQueueActor(
type: "ChatQueuedSendError",
content,
errorMessage,
...(getCharacterErrorCode(error)
? { characterErrorCode: getCharacterErrorCode(error) ?? undefined }
: {}),
});
} finally {
if (activeController === controller) activeController = null;
+16 -3
View File
@@ -10,6 +10,7 @@ import {
} from "../helper/unlock";
import type { ChatState } from "../chat-state";
import { baseChatMachineSetup } from "./setup";
import { getCharacterErrorCode } from "@/data/services/api";
const applyLocalHistoryLoadedAction = baseChatMachineSetup.assign(
({ event }) => {
@@ -48,9 +49,21 @@ const showUnlockHistoryPromptFromNetworkAction = baseChatMachineSetup.assign(
},
);
const markHistoryLoadFailedAction = baseChatMachineSetup.assign({
historyLoaded: true,
});
const markHistoryLoadFailedAction = baseChatMachineSetup.assign(
({ context, event }) => {
if (event.type !== "ChatHistoryLoadFailed") return {};
const characterErrorCode = getCharacterErrorCode(event.error);
return {
historyLoaded: true,
characterErrorCode,
canSendMessage:
characterErrorCode === "CHARACTER_DISABLED" ||
characterErrorCode === "CHARACTER_NOT_FOUND"
? false
: context.canSendMessage,
};
},
);
const markLoadMoreHistoryStartedAction = baseChatMachineSetup.assign({
isLoadingMoreHistory: true,
+17 -3
View File
@@ -76,16 +76,30 @@ const appendUserMessageAction = historyMachineSetup.assign(
);
const appendQueuedSendErrorMessageAction = historyMachineSetup.assign(
({ context }) => {
({ context, event }) => {
if (event.type !== "ChatQueuedSendError") return {};
const characterErrorCode = event.characterErrorCode ?? null;
const unavailable = characterErrorCode === "CHARACTER_STATE_UNAVAILABLE";
const messages = [
...context.messages,
{
content: "Something went wrong. Try sending again?",
content: unavailable
? "This character is temporarily unavailable. Please try again shortly."
: "Something went wrong. Try sending again?",
isFromAI: true,
date: todayString(),
},
];
return { messages, ...finishPendingReply(context) };
return {
messages,
...finishPendingReply(context),
characterErrorCode,
canSendMessage:
characterErrorCode === "CHARACTER_DISABLED" ||
characterErrorCode === "CHARACTER_NOT_FOUND"
? false
: context.canSendMessage,
};
},
);
+5
View File
@@ -189,6 +189,11 @@ const userSessionState = chatMachineSetup.createStateConfig({
},
],
on: {
ChatHistoryRefreshRequested: {
target: "#chat.userSession",
reenter: true,
actions: "startUserSession",
},
...historyPaginationTransitions,
ChatGuestLogin: {
target: "#chat.guestSession",
+3 -3
View File
@@ -16,15 +16,15 @@ import {
import type { ChatState } from "../chat-state";
export interface ChatMachineInput {
characterId?: string;
emptyChatGreeting?: string;
characterId: string;
emptyChatGreeting: string;
}
export const baseChatMachineSetup = setup({
types: {
context: {} as ChatState,
events: {} as ChatEvent,
input: {} as ChatMachineInput | undefined,
input: {} as ChatMachineInput,
},
actors: {
loadHistory: loadHistoryActor,
+21 -4
View File
@@ -9,6 +9,7 @@ import {
import type { UnlockHistoryOutput } from "./actors/unlock";
import { sendMachineSetup } from "./send-flow";
import { createChatActorActionSetup } from "./setup";
import { getCharacterErrorCode } from "@/data/services/api";
const markPaymentUnlockPendingAction = sendMachineSetup.assign(() => ({
paymentUnlockPending: true,
@@ -146,6 +147,13 @@ const clearUnlockPaywallRequestAction = sendMachineSetup.assign(() => ({
unlockPaywallRequest: null,
}));
const handleCharacterMismatchAction = sendMachineSetup.assign(() => ({
unlockingMessage: null,
unlockMessageError: "character_mismatch",
unlockPaywallRequest: null,
characterErrorCode: "CHARACTER_MISMATCH" as const,
}));
export const unlockMachineSetup = sendMachineSetup.extend({
actions: {
markPaymentUnlockPending: markPaymentUnlockPendingAction,
@@ -158,6 +166,7 @@ export const unlockMachineSetup = sendMachineSetup.extend({
requestUnlockPaymentFromOutput: requestUnlockPaymentFromOutputAction,
requestUnlockPaymentFromError: requestUnlockPaymentFromErrorAction,
clearUnlockPaywallRequest: clearUnlockPaywallRequestAction,
handleCharacterMismatch: handleCharacterMismatchAction,
},
});
@@ -225,9 +234,17 @@ export const unlockingMessageState = unlockMachineSetup.createStateConfig({
actions: "requestUnlockPaymentFromOutput",
},
],
onError: {
target: "ready",
actions: "requestUnlockPaymentFromError",
},
onError: [
{
guard: ({ event }) =>
getCharacterErrorCode(event.error) === "CHARACTER_MISMATCH",
target: "ready",
actions: "handleCharacterMismatch",
},
{
target: "ready",
actions: "requestUnlockPaymentFromError",
},
],
},
});