refactor(chat): synchronize guest chat quota updates for messages and images
This commit is contained in:
@@ -11,21 +11,7 @@
|
|||||||
* **状态结构**(parent state 模式):
|
* **状态结构**(parent state 模式):
|
||||||
* - `idle`:屏没挂 / 登出
|
* - `idle`:屏没挂 / 登出
|
||||||
* - `guestSession`(**parent**):游客会话 —— **不** invoke WS
|
* - `guestSession`(**parent**):游客会话 —— **不** invoke WS
|
||||||
* - `initializing`:**双** invoke + `always` barrier
|
|
||||||
* - `loadQuotaActor`(**纯** local,ChatStorage **日**配 + 总**配**)
|
|
||||||
* - `loadHistoryActor`(local → network → save 3 步)
|
|
||||||
* - 2 **个** `quotaLoaded` / `historyLoaded` **都** true **才**进** ready
|
|
||||||
* - `ready`:用户聊天 + 看消息
|
|
||||||
* - `sending`:invoke sendMessageHttpActor(HTTP 发送消息)
|
|
||||||
* - ~~`loadingMore`~~:**已**删**(游客**无**服**务**端** history,**不**支持翻**页**)
|
|
||||||
* - `userSession`(**parent**):非游客会话 —— **invoke WS**(持久)
|
* - `userSession`(**parent**):非游客会话 —— **invoke WS**(持久)
|
||||||
* - 父级 invoke `chatWebSocket`(**一**进**来****就**连)
|
|
||||||
* - `initializing`:invoke `loadHistory` + `always` barrier
|
|
||||||
* - WS **已**连** + history **加**载**完**成** → ready
|
|
||||||
* - `ready`:用户聊天 + 看消息
|
|
||||||
* - `sending`:invoke sendMessageHttpActor(HTTP fallback —— WS 真发**未** wire)
|
|
||||||
* - `loadingMore`:invoke loadMoreHistory(**翻**页**,**保**留**)
|
|
||||||
* WS 在 `userSession` parent 状态 invoke —— 跨 initializing/ready/sending/loadingMore 持续
|
|
||||||
*
|
*
|
||||||
* **init 双任务**(**核**心**重**构**):
|
* **init 双任务**(**核**心**重**构**):
|
||||||
* - guestSession.initializing:loadQuota + loadHistory **并**行**(`invoke: [...]` **数**组**)
|
* - guestSession.initializing:loadQuota + loadHistory **并**行**(`invoke: [...]` **数**组**)
|
||||||
@@ -42,20 +28,11 @@
|
|||||||
* - 非游客"无消息限制"(业务事实)—— 永不被赋值
|
* - 非游客"无消息限制"(业务事实)—— 永不被赋值
|
||||||
* - `appendUserMessage` 减 `context.wsConnected ? q : Math.max(0, q-1)` —— WS 已连时**不**递减
|
* - `appendUserMessage` 减 `context.wsConnected ? q : Math.max(0, q-1)` —— WS 已连时**不**递减
|
||||||
*
|
*
|
||||||
* **文件结构**:
|
|
||||||
* - `chat-machine.ts`(本文件)—— **只**含 setup + createMachine(actions + states)
|
|
||||||
* - `chat-machine.helpers.ts` —— 纯函数 + 数据加载(readGuestQuota + readAndSyncHistory)
|
|
||||||
* - `chat-machine.actors.ts` —— 5 个 XState actor(loadQuota + loadHistory + sendMessage + loadMoreHistory + chatWebSocket)
|
|
||||||
*
|
|
||||||
* **日**志**策略**(**清**理**后**):
|
|
||||||
* - **不**在**状**态**机**文**件**中**打**追踪**日**志**(actor / helper / action **本**身**有** ENTRY / DONE **日**志**,**就**够**排查**了)
|
|
||||||
* - `appendUserMessage` / `appendUserImage` / `appendOrUpdateAISentence` 这 3 **个** action `assign` **内**部**保**留** `console.log`(**记**录** action **本**身**的**业**务**决**策**,**比**如** quota 减**少** / isFromAI 标**记**)
|
|
||||||
* - **所**有** actor ENTRY / API / DONE **全**链**日**志**放**在** `chat-machine.actors.ts`
|
|
||||||
* - **所**有** helper 数据**加**载**日**志**(`readGuestQuota` / `readAndSyncHistory` 3 **步**)放**在** `chat-machine.helpers.ts`
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setup, assign } from "xstate";
|
import { setup, assign } from "xstate";
|
||||||
|
|
||||||
|
import { ChatStorage } from "@/data/storage/chat/chat_storage";
|
||||||
import { formatDate } from "@/utils/date";
|
import { formatDate } from "@/utils/date";
|
||||||
|
|
||||||
import { ChatState, initialState } from "./chat-state";
|
import { ChatState, initialState } from "./chat-state";
|
||||||
@@ -98,19 +75,28 @@ export const chatMachine = setup({
|
|||||||
appendUserMessage: assign(({ context, event }) => {
|
appendUserMessage: assign(({ context, event }) => {
|
||||||
if (event.type !== "ChatSendMessage") return {};
|
if (event.type !== "ChatSendMessage") return {};
|
||||||
|
|
||||||
const newQuota = context.wsConnected
|
// 两**个**额度**同**步**更**新**:WS 已**连****不**递减(**实**时**会**话**不**消耗**),HTTP **必**须**减
|
||||||
|
const newRemaining = context.wsConnected
|
||||||
? context.guestRemainingQuota
|
? context.guestRemainingQuota
|
||||||
: Math.max(0, context.guestRemainingQuota - 1);
|
: Math.max(0, context.guestRemainingQuota - 1);
|
||||||
|
const newTotal = context.wsConnected
|
||||||
|
? context.guestTotalQuota
|
||||||
|
: Math.max(0, context.guestTotalQuota - 1);
|
||||||
|
|
||||||
|
// 持**久**化**两**个**额度**(** fire-and-forget,**不** await —— assign **是** sync)
|
||||||
|
// daily quota 需**要** today **字**符**串**(**让** ChatStorage 跨**天**判**断** reset)
|
||||||
|
const today = formatDate();
|
||||||
|
ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
|
||||||
|
ChatStorage.getInstance().setGuestTotalQuota(newTotal);
|
||||||
|
|
||||||
console.log("[chat-machine] appendUserMessage", {
|
console.log("[chat-machine] appendUserMessage", {
|
||||||
contentLength: event.content.length,
|
contentLength: event.content.length,
|
||||||
contentPreview: event.content.slice(0, 50),
|
contentPreview: event.content.slice(0, 50),
|
||||||
oldMessagesCount: context.messages.length,
|
|
||||||
newMessagesCount: context.messages.length + 1,
|
|
||||||
wsConnected: context.wsConnected,
|
|
||||||
quotaMode: context.wsConnected ? "ws (no decrement)" : "http (decrement)",
|
quotaMode: context.wsConnected ? "ws (no decrement)" : "http (decrement)",
|
||||||
oldQuota: context.guestRemainingQuota,
|
oldRemaining: context.guestRemainingQuota,
|
||||||
newQuota,
|
newRemaining,
|
||||||
|
oldTotal: context.guestTotalQuota,
|
||||||
|
newTotal,
|
||||||
isReplyingAI: true,
|
isReplyingAI: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,25 +106,35 @@ export const chatMachine = setup({
|
|||||||
{
|
{
|
||||||
content: event.content,
|
content: event.content,
|
||||||
isFromAI: false,
|
isFromAI: false,
|
||||||
date: formatDate(),
|
date: today,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isReplyingAI: true,
|
isReplyingAI: true,
|
||||||
guestRemainingQuota: newQuota,
|
guestRemainingQuota: newRemaining,
|
||||||
|
guestTotalQuota: newTotal,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
appendUserImage: assign(({ context, event }) => {
|
appendUserImage: assign(({ context, event }) => {
|
||||||
if (event.type !== "ChatSendImage") return {};
|
if (event.type !== "ChatSendImage") return {};
|
||||||
const newQuota = context.wsConnected
|
// **同** appendUserMessage:两**个**额度**同**步**减
|
||||||
|
const newRemaining = context.wsConnected
|
||||||
? context.guestRemainingQuota
|
? context.guestRemainingQuota
|
||||||
: Math.max(0, context.guestRemainingQuota - 1);
|
: Math.max(0, context.guestRemainingQuota - 1);
|
||||||
|
const newTotal = context.wsConnected
|
||||||
|
? context.guestTotalQuota
|
||||||
|
: Math.max(0, context.guestTotalQuota - 1);
|
||||||
|
const today = formatDate();
|
||||||
|
void ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
|
||||||
|
void ChatStorage.getInstance().setGuestTotalQuota(newTotal);
|
||||||
console.log("[chat-machine] appendUserImage", {
|
console.log("[chat-machine] appendUserImage", {
|
||||||
oldMessagesCount: context.messages.length,
|
oldMessagesCount: context.messages.length,
|
||||||
wsConnected: context.wsConnected,
|
wsConnected: context.wsConnected,
|
||||||
quotaMode: context.wsConnected ? "ws (no decrement)" : "http (decrement)",
|
quotaMode: context.wsConnected ? "ws (no decrement)" : "http (decrement)",
|
||||||
oldQuota: context.guestRemainingQuota,
|
oldRemaining: context.guestRemainingQuota,
|
||||||
newQuota,
|
newRemaining,
|
||||||
|
oldTotal: context.guestTotalQuota,
|
||||||
|
newTotal,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
messages: [
|
messages: [
|
||||||
@@ -146,12 +142,13 @@ export const chatMachine = setup({
|
|||||||
{
|
{
|
||||||
content: "[Image]",
|
content: "[Image]",
|
||||||
isFromAI: false,
|
isFromAI: false,
|
||||||
date: formatDate(),
|
date: today,
|
||||||
imageUrl: event.imageBase64,
|
imageUrl: event.imageBase64,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isReplyingAI: true,
|
isReplyingAI: true,
|
||||||
guestRemainingQuota: newQuota,
|
guestRemainingQuota: newRemaining,
|
||||||
|
guestTotalQuota: newTotal,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -290,7 +287,6 @@ export const chatMachine = setup({
|
|||||||
guard: ({ event }) => event.content.trim().length > 0,
|
guard: ({ event }) => event.content.trim().length > 0,
|
||||||
target: "sending",
|
target: "sending",
|
||||||
},
|
},
|
||||||
// 兜**底**:**内**容**空** / 什**么**都**不**匹**配** → **静**默**丢**弃**
|
|
||||||
],
|
],
|
||||||
// **配**额**检**查** + 发**送**图**片**(**同**样** 3 **条** guard,**不**检**查** "内**容**空"**)
|
// **配**额**检**查** + 发**送**图**片**(**同**样** 3 **条** guard,**不**检**查** "内**容**空"**)
|
||||||
ChatSendImage: [
|
ChatSendImage: [
|
||||||
|
|||||||
Reference in New Issue
Block a user