refactor(chat): rely on server message limits
This commit is contained in:
+35
-124
@@ -15,36 +15,33 @@
|
||||
* - `nonVipUserSession`(parent):非 VIP 用户会话 —— 不 invoke WS
|
||||
* - `vipUserSession`(parent):VIP 用户会话 —— invoke WS(持久)
|
||||
*
|
||||
* init 双任务(核心重构):
|
||||
* - guestSession.initializing:loadQuota + loadHistory 并行(`invoke: [...]` 数组)
|
||||
* init 任务:
|
||||
* - guestSession.initializing:loadHistory
|
||||
* - nonVipUserSession.initializing:loadHistory
|
||||
* - vipUserSession.initializing:loadHistory + parent-level chatWebSocket 并行
|
||||
* - 每个任务都是独立 actor,不互相等待(除非 `always` barrier)
|
||||
*
|
||||
* 消息发送路径(业务事实):
|
||||
* - guestSession:走 HTTP,并先检查本地游客配额
|
||||
* - guestSession:走 HTTP,消息数量限制以后端 `blocked` 响应为准
|
||||
* - nonVipUserSession:走 HTTP,让后端返回 daily_limit blocked
|
||||
* - vipUserSession + WS 已连:走 WS,不受每日免费次数限制
|
||||
* - `ChatWebSocketConnected` 事件 → `wsConnected = true`
|
||||
* - `vipUserSession.exit` action → `wsConnected = false`(WS actor cleanup 时也会跑 exit)
|
||||
*
|
||||
* 配额:
|
||||
* - 游客:保留本地配额 guard,命中后不发送消息
|
||||
* - 注册非 VIP:以后端 `blocked=true && blockReason="daily_limit"` 为准
|
||||
* - `appendUserMessage` 减 `context.wsConnected ? q : Math.max(0, q-1)` —— WS 已连时不递减
|
||||
* - 前端不再处理本地消息额度;游客 / 注册用户均以后端响应为准。
|
||||
* - 后端返回 `blocked=true && blockReason="daily_limit"` 时展示会员引导。
|
||||
*
|
||||
*/
|
||||
|
||||
import { setup, assign } from "xstate";
|
||||
|
||||
import { ChatStorage } from "@/data/storage/chat/chat_storage";
|
||||
import { todayString, Logger } from "@/utils";
|
||||
|
||||
import { ChatState, initialState } from "./chat-state";
|
||||
import type { ChatEvent } from "./chat-events";
|
||||
import { applyHttpSendOutput } from "./chat-machine.helpers";
|
||||
import {
|
||||
loadQuotaActor,
|
||||
loadHistoryActor,
|
||||
sendMessageHttpActor,
|
||||
sendMessageWsActor,
|
||||
@@ -68,7 +65,6 @@ export const chatMachine = setup({
|
||||
events: {} as ChatEvent,
|
||||
},
|
||||
actors: {
|
||||
loadQuota: loadQuotaActor,
|
||||
loadHistory: loadHistoryActor,
|
||||
sendMessageHttp: sendMessageHttpActor,
|
||||
sendMessageWs: sendMessageWsActor,
|
||||
@@ -91,25 +87,11 @@ export const chatMachine = setup({
|
||||
appendGuestUserMessage: assign(({ context, event }) => {
|
||||
if (event.type !== "ChatSendMessage") return {};
|
||||
|
||||
// 游客消息发送成功前先乐观扣减本地日配额 + 总配额。
|
||||
const newRemaining = Math.max(0, context.guestRemainingQuota - 1);
|
||||
const newTotal = Math.max(0, context.guestTotalQuota - 1);
|
||||
|
||||
// 持久化两个额度( fire-and-forget,不 await —— assign 是 sync)
|
||||
// daily quota 需要 today 字符串(让 ChatStorage 跨天判断 reset)
|
||||
const today = todayString();
|
||||
ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
|
||||
|
||||
ChatStorage.getInstance().setGuestTotalQuota(newTotal);
|
||||
|
||||
log.debug("[chat-machine] appendGuestUserMessage", {
|
||||
contentLength: event.content.length,
|
||||
contentPreview: event.content.slice(0, 50),
|
||||
quotaMode: "guest http (decrement)",
|
||||
oldRemaining: context.guestRemainingQuota,
|
||||
newRemaining,
|
||||
oldTotal: context.guestTotalQuota,
|
||||
newTotal,
|
||||
quotaMode: "server controlled",
|
||||
isReplyingAI: true,
|
||||
});
|
||||
|
||||
@@ -123,8 +105,6 @@ export const chatMachine = setup({
|
||||
},
|
||||
],
|
||||
isReplyingAI: true,
|
||||
guestRemainingQuota: newRemaining,
|
||||
guestTotalQuota: newTotal,
|
||||
paywallTriggered: false,
|
||||
paywallReason: null,
|
||||
paywallDetail: null,
|
||||
@@ -160,22 +140,11 @@ export const chatMachine = setup({
|
||||
|
||||
appendGuestUserImage: assign(({ context, event }) => {
|
||||
if (event.type !== "ChatSendImage") return {};
|
||||
// 同 appendGuestUserMessage:游客两个额度同步减
|
||||
const newRemaining = Math.max(0, context.guestRemainingQuota - 1);
|
||||
const newTotal = Math.max(0, context.guestTotalQuota - 1);
|
||||
|
||||
const today = todayString();
|
||||
void ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
|
||||
|
||||
void ChatStorage.getInstance().setGuestTotalQuota(newTotal);
|
||||
|
||||
const today = todayString();
|
||||
log.debug("[chat-machine] appendGuestUserImage", {
|
||||
oldMessagesCount: context.messages.length,
|
||||
quotaMode: "guest http (decrement)",
|
||||
oldRemaining: context.guestRemainingQuota,
|
||||
newRemaining,
|
||||
oldTotal: context.guestTotalQuota,
|
||||
newTotal,
|
||||
quotaMode: "server controlled",
|
||||
});
|
||||
return {
|
||||
messages: [
|
||||
@@ -188,8 +157,6 @@ export const chatMachine = setup({
|
||||
},
|
||||
],
|
||||
isReplyingAI: true,
|
||||
guestRemainingQuota: newRemaining,
|
||||
guestTotalQuota: newTotal,
|
||||
paywallTriggered: false,
|
||||
paywallReason: null,
|
||||
paywallDetail: null,
|
||||
@@ -309,10 +276,6 @@ export const chatMachine = setup({
|
||||
};
|
||||
}),
|
||||
|
||||
incrementQuotaExceeded: assign(({ context }) => ({
|
||||
quotaExceededTrigger: context.quotaExceededTrigger + 1,
|
||||
})),
|
||||
|
||||
setWsConnected: assign({ wsConnected: true }),
|
||||
clearWsConnected: assign({ wsConnected: false }),
|
||||
},
|
||||
@@ -356,94 +319,44 @@ export const chatMachine = setup({
|
||||
initial: "initializing",
|
||||
states: {
|
||||
initializing: {
|
||||
// "always" barrier:两个任务都完成才进 ready
|
||||
// 本地游客额度逻辑已停用:只等待历史加载完成。
|
||||
always: [
|
||||
{
|
||||
target: "ready",
|
||||
guard: ({ context }) => context.quotaLoaded && context.historyLoaded,
|
||||
guard: ({ context }) => context.historyLoaded,
|
||||
},
|
||||
],
|
||||
invoke: [
|
||||
// 任务 1:加载配额(fromPromise,返结果 + onDone 设 flag)
|
||||
{
|
||||
id: "loadQuota",
|
||||
src: "loadQuota",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => ({
|
||||
guestRemainingQuota: event.output.remaining,
|
||||
guestTotalQuota: event.output.total,
|
||||
quotaLoaded: true,
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
// 失败也标 loaded,不卡 init(游客可能 quota 服务不可用,让 UI 进 ready)
|
||||
actions: assign({
|
||||
quotaLoaded: true,
|
||||
}),
|
||||
},
|
||||
invoke: {
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => ({
|
||||
messages: event.output.messages,
|
||||
hasMore: event.output.hasMore,
|
||||
historyOffset: event.output.newOffset,
|
||||
historyLoaded: true,
|
||||
})),
|
||||
},
|
||||
// 任务 2:拉 history(fromPromise,返回最终 network messages)
|
||||
{
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => ({
|
||||
messages: event.output.messages,
|
||||
hasMore: event.output.hasMore,
|
||||
historyOffset: event.output.newOffset,
|
||||
historyLoaded: true,
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
// 失败也标 loaded,不卡 init(让 UI 还是能进 ready,屏幕可能空)
|
||||
actions: assign({
|
||||
historyLoaded: true,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
// 失败也标 loaded,不卡 init(让 UI 还是能进 ready,屏幕可能空)
|
||||
actions: assign({
|
||||
historyLoaded: true,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
on: {
|
||||
// 配额检查 + 发送消息(3 条 guard,按顺序匹配)
|
||||
// 1. 总配额(guestTotalQuota)不足 → 触发 quota exceeded dialog,不发送
|
||||
// 2. 每日配额(guestRemainingQuota)不足 → 同上
|
||||
// 3. 两个配额都 OK + 内容非空 → 发送 + 减配额
|
||||
ChatSendMessage: [
|
||||
// 1. 总配额 不足(guestTotalQuota <= 0) → 触发 quota exceeded dialog
|
||||
{
|
||||
guard: ({ context }) => context.guestTotalQuota <= 0,
|
||||
actions: "incrementQuotaExceeded",
|
||||
},
|
||||
// 2. 每日配额 不足(guestRemainingQuota <= 0) → 触发 quota exceeded dialog
|
||||
{
|
||||
guard: ({ context }) => context.guestRemainingQuota <= 0,
|
||||
actions: "incrementQuotaExceeded",
|
||||
},
|
||||
// 3. 两个配额都 OK + 内容非空 → 发送(只用 appendUserMessage 自己的日志)
|
||||
{
|
||||
actions: "appendGuestUserMessage",
|
||||
guard: ({ event }) => event.content.trim().length > 0,
|
||||
target: "sending",
|
||||
},
|
||||
],
|
||||
// 配额检查 + 发送图片(同样 3 条 guard,不检查 "内容空")
|
||||
ChatSendImage: [
|
||||
{
|
||||
guard: ({ context }) => context.guestTotalQuota <= 0,
|
||||
actions: "incrementQuotaExceeded",
|
||||
},
|
||||
{
|
||||
guard: ({ context }) => context.guestRemainingQuota <= 0,
|
||||
actions: "incrementQuotaExceeded",
|
||||
},
|
||||
{
|
||||
actions: "appendGuestUserImage",
|
||||
target: "sending",
|
||||
},
|
||||
],
|
||||
ChatSendMessage: {
|
||||
actions: "appendGuestUserMessage",
|
||||
guard: ({ event }) => event.content.trim().length > 0,
|
||||
target: "sending",
|
||||
},
|
||||
ChatSendImage: {
|
||||
actions: "appendGuestUserImage",
|
||||
target: "sending",
|
||||
},
|
||||
// 删除 ChatLoadMoreHistory / WS handlers —— 游客无服务端 history,也不连接 WS
|
||||
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
|
||||
},
|
||||
},
|
||||
sending: {
|
||||
@@ -524,7 +437,6 @@ export const chatMachine = setup({
|
||||
ChatLoadMoreHistory: {
|
||||
target: "loadingMore",
|
||||
},
|
||||
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
|
||||
},
|
||||
},
|
||||
sendingViaHttp: {
|
||||
@@ -665,7 +577,6 @@ export const chatMachine = setup({
|
||||
ChatLoadMoreHistory: {
|
||||
target: "loadingMore",
|
||||
},
|
||||
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
|
||||
// 注:ChatAISentenceReceived / ChatWebSocketError / ChatWebSocketConnected
|
||||
// 已上提到 vipUserSession.on,子级不再声明
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user