refactor(chat): split sessions by vip status

This commit is contained in:
2026-06-22 15:11:57 +08:00
parent b105ba3457
commit 6ff1accad5
5 changed files with 225 additions and 81 deletions
+1 -2
View File
@@ -58,9 +58,8 @@ export function ChatAuthSync() {
const tokenR = await AuthStorage.getInstance().getLoginToken(); const tokenR = await AuthStorage.getInstance().getLoginToken();
if (!cancelled && tokenR.success && tokenR.data) { if (!cancelled && tokenR.success && tokenR.data) {
chatDispatch({ chatDispatch({
type: "ChatNonGuestLogin", type: isVip ? "ChatVipLogin" : "ChatNonVipLogin",
token: tokenR.data, token: tokenR.data,
isVip,
}); });
} }
})(); })();
+4 -2
View File
@@ -9,7 +9,8 @@
* *
* 鉴权生命周期事件(本轮新增): * 鉴权生命周期事件(本轮新增):
* - `ChatGuestLogin`:游客进入 /chat —— 拉本地消息 + 初始化配额(不连 WS) * - `ChatGuestLogin`:游客进入 /chat —— 拉本地消息 + 初始化配额(不连 WS)
* - `ChatNonGuestLogin`:非游客进入 /chat —— 拉服务器端首屏 + 连 WStoken 在 payload * - `ChatNonVipLogin`:非 VIP 用户进入 /chat —— 拉服务器端首屏,不连 WS
* - `ChatVipLogin`VIP 用户进入 /chat —— 拉服务器端首屏 + 连 WStoken 在 payload
* - `ChatLogout`:登出 / 切换用户 —— 断 WS + 清消息 * - `ChatLogout`:登出 / 切换用户 —— 断 WS + 清消息
* *
* 设计:所有WS / 历史 / 配额相关副作用全部通过派发事件给 chat 机器处理, * 设计:所有WS / 历史 / 配额相关副作用全部通过派发事件给 chat 机器处理,
@@ -18,7 +19,8 @@
export type ChatEvent = export type ChatEvent =
// 鉴权生命周期(登录态变化后由 ChatAuthSync 派) // 鉴权生命周期(登录态变化后由 ChatAuthSync 派)
| { type: "ChatGuestLogin" } | { type: "ChatGuestLogin" }
| { type: "ChatNonGuestLogin"; token: string; isVip: boolean } | { type: "ChatNonVipLogin"; token: string }
| { type: "ChatVipLogin"; token: string }
| { type: "ChatLogout" } | { type: "ChatLogout" }
// 业务事件 // 业务事件
| { type: "ChatSendMessage"; content: string } | { type: "ChatSendMessage"; content: string }
+4 -4
View File
@@ -31,8 +31,8 @@ const log = new Logger("StoresChatChatMachineActors");
* 为什么用 module-level 而非 context.wsRef * 为什么用 module-level 而非 context.wsRef
* 1) `ChatWebSocket` 是带回调 + 重连 timer 的非可序列化对象,放进 * 1) `ChatWebSocket` 是带回调 + 重连 timer 的非可序列化对象,放进
* XState context 会污染快照、警告 non-serializable * XState context 会污染快照、警告 non-serializable
* 2) userSession 同一时间只可能有一个活跃 WSparent-level invoke), * 2) vipUserSession 同一时间只可能有一个活跃 WSparent-level invoke),
* module 单例的语义和"userSession 内一个活跃连接"一一对应 * module 单例的语义和"VIP 会话内一个活跃连接"一一对应
* 3) 不需要重构 ChatWebSocket 类的对外 API * 3) 不需要重构 ChatWebSocket 类的对外 API
*/ */
let activeChatWebSocket: ChatWebSocket | null = null; let activeChatWebSocket: ChatWebSocket | null = null;
@@ -123,7 +123,7 @@ export const loadMoreHistoryActor = fromPromise<
// WebSocket: long-lived callback // WebSocket: long-lived callback
// ============================================================ // ============================================================
/** /**
* 长生命周期 WebSocket(仅 `userSession` 期间 invoke * 长生命周期 WebSocket(仅 `vipUserSession` 期间 invoke
* - 启动:连 WS,事件 → sendBack 派回机器 * - 启动:连 WS,事件 → sendBack 派回机器
* - 停止(cleanup):disconnect * - 停止(cleanup):disconnect
* - 游客不 invoke 此 actor(业务事实"游客不连 WS" * - 游客不 invoke 此 actor(业务事实"游客不连 WS"
@@ -170,7 +170,7 @@ export const chatWebSocketActor = fromCallback<ChatEvent, { token: string }>(
// WebSocket: per-send callback // WebSocket: per-send callback
// ============================================================ // ============================================================
/** /**
* WS 发送消息(userSession.sendingViaWs 期间 invoke * WS 发送消息(vipUserSession.sendingViaWs 期间 invoke
* - 走 `activeChatWebSocket.sendMessage(content)` * - 走 `activeChatWebSocket.sendMessage(content)`
* - 成功后 AI 回复通过 `chatWebSocketActor` 的 `onSentence` → `ChatAISentenceReceived` 流回 * - 成功后 AI 回复通过 `chatWebSocketActor` 的 `onSentence` → `ChatAISentenceReceived` 流回
* - 失败抛错 → state machine 的 `onError` 兜底(清 isReplyingAI、回 ready * - 失败抛错 → state machine 的 `onError` 兜底(清 isReplyingAI、回 ready
+207 -58
View File
@@ -3,27 +3,30 @@
* *
* 鉴权解耦(事件驱动): * 鉴权解耦(事件驱动):
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 <ChatAuthSync /> 派生 loginStatus * chat 机器不感知鉴权 / 不管 WebSocket —— 由 <ChatAuthSync /> 派生 loginStatus
* ChatAuthSync 只派 3 个事件: * ChatAuthSync 派发登录态生命周期事件:
* - `ChatGuestLogin` → 游客会话(断 WS = 不连) * - `ChatGuestLogin` → 游客会话(断 WS = 不连)
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS * - `ChatNonVipLogin { token }` → 非 VIP 用户会话(连 WS
* - `ChatVipLogin { token }` → VIP 用户会话(连 WS
* - `ChatLogout` → 登出(机器自动 cleanup WS actor * - `ChatLogout` → 登出(机器自动 cleanup WS actor
* *
* 状态结构(parent state 模式): * 状态结构(parent state 模式):
* - `idle`:屏没挂 / 登出 * - `idle`:屏没挂 / 登出
* - `guestSession`parent):游客会话 —— 不 invoke WS * - `guestSession`parent):游客会话 —— 不 invoke WS
* - `userSession`parent):非游客会话 —— invoke WS(持久) * - `nonVipUserSession`parent):非 VIP 用户会话 —— invoke WS
* - `vipUserSession`parent):VIP 用户会话 —— invoke WS(持久)
* *
* init 双任务(核心重构): * init 双任务(核心重构):
* - guestSession.initializingloadQuota + loadHistory 并行(`invoke: [...]` 数组) * - guestSession.initializingloadQuota + loadHistory 并行(`invoke: [...]` 数组)
* - userSession.initializingloadHistory + parent-level chatWebSocket 并行 * - nonVipUserSession.initializingloadHistory
* - vipUserSession.initializingloadHistory + parent-level chatWebSocket 并行
* - 每个任务都是独立 actor,不互相等待(除非 `always` barrier * - 每个任务都是独立 actor,不互相等待(除非 `always` barrier
* *
* 消息发送路径(业务事实): * 消息发送路径(业务事实):
* - guestSession:走 HTTP,并先检查本地游客配额 * - guestSession:走 HTTP,并先检查本地游客配额
* - userSession + VIP + WS 已连:走 WS,不受每日免费次数限制 * - nonVipUserSession:走 HTTP,让后端返回 daily_limit blocked
* - userSession + 非 VIP:走 HTTP,让后端返回 daily_limit blocked * - vipUserSession + WS 已连:走 WS,不受每日免费次数限制
* - `ChatWebSocketConnected` 事件 → `wsConnected = true` * - `ChatWebSocketConnected` 事件 → `wsConnected = true`
* - `userSession.exit` action → `wsConnected = false`WS actor cleanup 时也会跑 exit * - `vipUserSession.exit` action → `wsConnected = false`WS actor cleanup 时也会跑 exit
* *
* 配额: * 配额:
* - 游客:保留本地配额 guard,命中后不发送消息 * - 游客:保留本地配额 guard,命中后不发送消息
@@ -75,31 +78,22 @@ export const chatMachine = setup({
actions: { actions: {
startGuestSession: assign(() => ({ startGuestSession: assign(() => ({
...initialState, ...initialState,
isVip: false,
})), })),
startUserSession: assign(({ event }) => { startUserSession: assign(() => ({
if (event.type !== "ChatNonGuestLogin") return {}; ...initialState,
return { })),
...initialState,
isVip: event.isVip,
};
}),
clearChatSession: assign(() => ({ clearChatSession: assign(() => ({
...initialState, ...initialState,
})), })),
appendUserMessage: assign(({ context, event }) => { appendGuestUserMessage: assign(({ context, event }) => {
if (event.type !== "ChatSendMessage") return {}; if (event.type !== "ChatSendMessage") return {};
// 两个额度同步更新:WS 已连不递减(实时会话不消耗),HTTP 必须减 // 游客消息发送成功前先乐观扣减本地日配额 + 总配额。
const newRemaining = context.wsConnected const newRemaining = Math.max(0, context.guestRemainingQuota - 1);
? context.guestRemainingQuota const newTotal = Math.max(0, context.guestTotalQuota - 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 // 持久化两个额度( fire-and-forget,不 await —— assign 是 sync
// daily quota 需要 today 字符串(让 ChatStorage 跨天判断 reset // daily quota 需要 today 字符串(让 ChatStorage 跨天判断 reset
@@ -108,10 +102,10 @@ export const chatMachine = setup({
ChatStorage.getInstance().setGuestTotalQuota(newTotal); ChatStorage.getInstance().setGuestTotalQuota(newTotal);
log.debug("[chat-machine] appendUserMessage", { log.debug("[chat-machine] appendGuestUserMessage", {
contentLength: event.content.length, contentLength: event.content.length,
contentPreview: event.content.slice(0, 50), contentPreview: event.content.slice(0, 50),
quotaMode: context.wsConnected ? "ws (no decrement)" : "http (decrement)", quotaMode: "guest http (decrement)",
oldRemaining: context.guestRemainingQuota, oldRemaining: context.guestRemainingQuota,
newRemaining, newRemaining,
oldTotal: context.guestTotalQuota, oldTotal: context.guestTotalQuota,
@@ -137,25 +131,47 @@ export const chatMachine = setup({
}; };
}), }),
appendUserImage: assign(({ context, event }) => { 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),
wsConnected: context.wsConnected,
isReplyingAI: true,
});
return {
messages: [
...context.messages,
{
content: event.content,
isFromAI: false,
date: today,
},
],
isReplyingAI: true,
paywallTriggered: false,
paywallReason: null,
paywallDetail: null,
};
}),
appendGuestUserImage: assign(({ context, event }) => {
if (event.type !== "ChatSendImage") return {}; if (event.type !== "ChatSendImage") return {};
// 同 appendUserMessage:两个额度同步减 // 同 appendGuestUserMessage游客两个额度同步减
const newRemaining = context.wsConnected const newRemaining = Math.max(0, context.guestRemainingQuota - 1);
? context.guestRemainingQuota const newTotal = Math.max(0, context.guestTotalQuota - 1);
: Math.max(0, context.guestRemainingQuota - 1);
const newTotal = context.wsConnected
? context.guestTotalQuota
: Math.max(0, context.guestTotalQuota - 1);
const today = todayString(); const today = todayString();
void ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today); void ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
void ChatStorage.getInstance().setGuestTotalQuota(newTotal); void ChatStorage.getInstance().setGuestTotalQuota(newTotal);
log.debug("[chat-machine] appendUserImage", { log.debug("[chat-machine] appendGuestUserImage", {
oldMessagesCount: context.messages.length, oldMessagesCount: context.messages.length,
wsConnected: context.wsConnected, quotaMode: "guest http (decrement)",
quotaMode: context.wsConnected ? "ws (no decrement)" : "http (decrement)",
oldRemaining: context.guestRemainingQuota, oldRemaining: context.guestRemainingQuota,
newRemaining, newRemaining,
oldTotal: context.guestTotalQuota, oldTotal: context.guestTotalQuota,
@@ -180,6 +196,32 @@ export const chatMachine = setup({
}; };
}), }),
appendUserImage: assign(({ context, event }) => {
if (event.type !== "ChatSendImage") return {};
const today = todayString();
log.debug("[chat-machine] appendUserImage", {
oldMessagesCount: context.messages.length,
wsConnected: context.wsConnected,
});
return {
messages: [
...context.messages,
{
content: "[Image]",
isFromAI: false,
date: today,
imageUrl: event.imageBase64,
},
],
isReplyingAI: true,
paywallTriggered: false,
paywallReason: null,
paywallDetail: null,
};
}),
appendOrUpdateAISentence: assign(({ context, event }) => { appendOrUpdateAISentence: assign(({ context, event }) => {
if (event.type !== "ChatAISentenceReceived") return {}; if (event.type !== "ChatAISentenceReceived") return {};
const messages = [...context.messages]; const messages = [...context.messages];
@@ -237,8 +279,12 @@ export const chatMachine = setup({
target: "#chat.guestSession", target: "#chat.guestSession",
actions: "startGuestSession", actions: "startGuestSession",
}, },
ChatNonGuestLogin: { ChatNonVipLogin: {
target: "#chat.userSession", target: "#chat.nonVipUserSession",
actions: "startUserSession",
},
ChatVipLogin: {
target: "#chat.vipUserSession",
actions: "startUserSession", actions: "startUserSession",
}, },
}, },
@@ -250,8 +296,12 @@ export const chatMachine = setup({
target: "#chat.idle", target: "#chat.idle",
actions: "clearChatSession", actions: "clearChatSession",
}, },
ChatNonGuestLogin: { ChatNonVipLogin: {
target: "#chat.userSession", target: "#chat.nonVipUserSession",
actions: "startUserSession",
},
ChatVipLogin: {
target: "#chat.vipUserSession",
actions: "startUserSession", actions: "startUserSession",
}, },
}, },
@@ -324,7 +374,7 @@ export const chatMachine = setup({
}, },
// 3. 两个配额都 OK + 内容非空 → 发送(只用 appendUserMessage 自己的日志) // 3. 两个配额都 OK + 内容非空 → 发送(只用 appendUserMessage 自己的日志)
{ {
actions: "appendUserMessage", actions: "appendGuestUserMessage",
guard: ({ event }) => event.content.trim().length > 0, guard: ({ event }) => event.content.trim().length > 0,
target: "sending", target: "sending",
}, },
@@ -340,14 +390,11 @@ export const chatMachine = setup({
actions: "incrementQuotaExceeded", actions: "incrementQuotaExceeded",
}, },
{ {
actions: "appendUserImage", actions: "appendGuestUserImage",
target: "sending", target: "sending",
}, },
], ],
// 删除 ChatLoadMoreHistory handler —— 游客无服务端 history,不支持翻页 // 删除 ChatLoadMoreHistory / WS handlers —— 游客无服务端 history也不连接 WS
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
ChatWebSocketConnected: { actions: "setWsConnected" },
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" }, ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
}, },
}, },
@@ -372,11 +419,110 @@ export const chatMachine = setup({
}, },
}, },
userSession: { nonVipUserSession: {
// 父级 on:把 WS / AI 流句 事件从 ready.on 上提到 userSession.on on: {
ChatLogout: {
target: "#chat.idle",
actions: "clearChatSession",
},
ChatGuestLogin: {
target: "#chat.guestSession",
actions: "startGuestSession",
},
ChatNonVipLogin: {
target: "#chat.nonVipUserSession",
reenter: true,
actions: "startUserSession",
},
ChatVipLogin: {
target: "#chat.vipUserSession",
actions: "startUserSession",
},
},
initial: "initializing",
states: {
initializing: {
invoke: {
src: "loadHistory",
onDone: {
target: "ready",
actions: assign(({ event }) => ({
messages: event.output.messages,
isLoadingMore: false,
hasMore: event.output.hasMore,
historyOffset: event.output.newOffset,
historyLoaded: true,
})),
},
onError: {
target: "ready",
actions: assign({
isLoadingMore: false,
historyLoaded: true,
}),
},
},
},
ready: {
on: {
ChatSendMessage: {
guard: ({ event }) => event.content.trim().length > 0,
actions: "appendUserMessage",
target: "sendingViaHttp",
},
ChatSendImage: {
actions: "appendUserImage",
},
ChatLoadMoreHistory: {
target: "loadingMore",
},
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
},
},
sendingViaHttp: {
invoke: {
src: "sendMessageHttp",
input: ({ event }) => ({
content: event.type === "ChatSendMessage" ? event.content : "",
}),
onDone: {
target: "ready",
actions: assign(({ context, event }) =>
applyHttpSendOutput(context, event.output),
),
},
onError: {
target: "ready",
actions: assign({ isReplyingAI: false }),
},
},
},
loadingMore: {
invoke: {
src: "loadMoreHistory",
input: ({ context }) => ({ offset: context.historyOffset }),
onDone: {
target: "ready",
actions: assign(({ context, event }) => ({
messages: [...event.output.messages, ...context.messages],
isLoadingMore: false,
hasMore: event.output.hasMore,
historyOffset: event.output.newOffset,
})),
},
onError: {
target: "ready",
actions: assign({ isLoadingMore: false }),
},
},
},
},
},
vipUserSession: {
// 父级 on:把 WS / AI 流句 事件从 ready.on 上提到 vipUserSession.on
// —— 任何 child stateinitializing / ready / sendingViaWs / sendingViaHttp / loadingMore // —— 任何 child stateinitializing / ready / sendingViaWs / sendingViaHttp / loadingMore
// 都能收到 `ChatAISentenceReceived` 并把 AI 句 push 到 messages // 都能收到 `ChatAISentenceReceived` 并把 AI 句 push 到 messages
// 这点是 sendingViaWs 流式回 reply 的关键(machine 当时在 sendingViaWs
// //
// 子级 override 规则: // 子级 override 规则:
// - sendingViaWs 重新声明 `ChatWebSocketError` 加 target: "ready"(带 action // - sendingViaWs 重新声明 `ChatWebSocketError` 加 target: "ready"(带 action
@@ -390,8 +536,12 @@ export const chatMachine = setup({
target: "#chat.guestSession", target: "#chat.guestSession",
actions: "startGuestSession", actions: "startGuestSession",
}, },
ChatNonGuestLogin: { ChatNonVipLogin: {
target: "#chat.userSession", target: "#chat.nonVipUserSession",
actions: "startUserSession",
},
ChatVipLogin: {
target: "#chat.vipUserSession",
reenter: true, reenter: true,
actions: "startUserSession", actions: "startUserSession",
}, },
@@ -405,7 +555,7 @@ export const chatMachine = setup({
// 父级:WS 长连接(一进来就连,跨 initializing/ready/sending*/loadingMore // 父级:WS 长连接(一进来就连,跨 initializing/ready/sending*/loadingMore
src: "chatWebSocket", src: "chatWebSocket",
input: ({ event }) => ({ input: ({ event }) => ({
token: event.type === "ChatNonGuestLogin" ? event.token : "", token: event.type === "ChatVipLogin" ? event.token : "",
}), }),
}, },
states: { states: {
@@ -445,9 +595,8 @@ export const chatMachine = setup({
// 内容非空;wsConnected=false 同理) // 内容非空;wsConnected=false 同理)
ChatSendMessage: [ ChatSendMessage: [
{ {
// VIP 用户保留 WS非 VIP 走 HTTP,让后端 daily_limit 能返回 blocked // VIP 用户优先走 WSWS 尚未可用时由下一个 branch 走 HTTP fallback
guard: ({ context, event }) => guard: ({ context, event }) =>
context.isVip &&
context.wsConnected && context.wsConnected &&
event.content.trim().length > 0, event.content.trim().length > 0,
actions: "appendUserMessage", actions: "appendUserMessage",
@@ -468,16 +617,16 @@ export const chatMachine = setup({
}, },
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" }, ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
// 注:ChatAISentenceReceived / ChatWebSocketError / ChatWebSocketConnected // 注:ChatAISentenceReceived / ChatWebSocketError / ChatWebSocketConnected
// 已上提到 userSession.on,子级不再声明 // 已上提到 vipUserSession.on,子级不再声明
}, },
}, },
// WS 发送:invoke sendMessageWsActor 触发一次 send // WS 发送:invoke sendMessageWsActor 触发一次 send
// 之后 AI 流式回 reply 走 userSession.on 的 ChatAISentenceReceived → appendOrUpdateAISentence // 之后 AI 流式回 reply 走 vipUserSession.on 的 ChatAISentenceReceived → appendOrUpdateAISentence
// 当句尾 done: true → isReplyingAI = false → always 跳回 ready // 当句尾 done: true → isReplyingAI = false → always 跳回 ready
sendingViaWs: { sendingViaWs: {
on: { on: {
// 流式回包中 WS 出错(mid-stream → 加错误泡 + 跳回 ready(不卡在 sendingViaWs // 流式回包中 WS 出错(mid-stream → 加错误泡 + 跳回 ready(不卡在 sendingViaWs
// 这个 child handler 替换 userSession.on 的 ChatWebSocketErrorXState v5 不合并) // 这个 child handler 替换 vipUserSession.on 的 ChatWebSocketErrorXState v5 不合并)
// 所以需要重复声明 action: "appendSocketErrorMessage" // 所以需要重复声明 action: "appendSocketErrorMessage"
ChatWebSocketError: { ChatWebSocketError: {
target: "ready", target: "ready",
+9 -15
View File
@@ -1,13 +1,11 @@
/** /**
* Chat 状态机:State 形状 + 初始值 * Chat 状态机:State 形状 + 初始值
* *
* 注:GuestChatQuota 仍位于 chat-machine.ts(属于业务常量,与上下文配对紧密)。 * `wsConnected: boolean` 仅 VIP 会话期间有意义:
* * - VIP + WS 已连 → 消息发送走 WS
* 新增 `wsConnected: boolean`(仅 userSession 期间 true): * - 非 VIP / 游客 → 不连接 WS,消息走 HTTP
* - WS 已连 → 消息发送走 WS,不消耗次数
* - WS 未连 → 消息发送走 HTTP,消耗次数
* - `ChatWebSocketConnected` 事件 → true * - `ChatWebSocketConnected` 事件 → true
* - `userSession.exit` → falseWS actor cleanup 时也会跑 exit * - VIP 会话 exit → falseWS actor cleanup 时也会跑 exit
*/ */
import type { UiMessage } from "@/data/dto/chat"; import type { UiMessage } from "@/data/dto/chat";
@@ -15,14 +13,11 @@ export interface ChatState {
messages: UiMessage[]; messages: UiMessage[];
isReplyingAI: boolean; isReplyingAI: boolean;
/** /**
* WebSocket 连接状态(仅 userSession 期间 true * WebSocket 连接状态(仅 VIP 用户会话期间 true
* - 决定消息发送路径(WS / HTTP)和是否消耗次数 * - VIP true:走 WS
* - true:走 WS,不消耗次数 * - 非 VIP / 游客:固定 false,走 HTTP
* - false:走 HTTP,消耗次数
*/ */
wsConnected: boolean; wsConnected: boolean;
/** 当前非游客用户是否 VIP;游客会话固定 false。 */
isVip: boolean;
/** 游客剩余配额(次/天)—— 仅游客业务展示用 /** 游客剩余配额(次/天)—— 仅游客业务展示用
*/ */
guestRemainingQuota: number; guestRemainingQuota: number;
@@ -37,11 +32,11 @@ export interface ChatState {
historyOffset: number; historyOffset: number;
/** 游客配额加载完成标志(仅 guestSession 期间) —— default false /** 游客配额加载完成标志(仅 guestSession 期间) —— default false
* - guestSession.initializing 走 always barrier`quotaLoaded && historyLoaded` 才进 ready * - guestSession.initializing 走 always barrier`quotaLoaded && historyLoaded` 才进 ready
* - userSession 永不为 true(不调 loadQuota)—— OK,不影响 barrier * - user sessions 永不为 true(不调 loadQuota)—— OK,不影响 barrier
*/ */
quotaLoaded: boolean; quotaLoaded: boolean;
/** history 加载完成标志(initial load —— local → network → save 跑完) /** history 加载完成标志(initial load —— local → network → save 跑完)
* - guestSession.initializing / userSession.initializing 走 always barrier * - guestSession.initializing / user sessions initializing 走 always barrier
* - 不被 `loadMoreHistoryActor` 设置(翻页是另一码事) * - 不被 `loadMoreHistoryActor` 设置(翻页是另一码事)
*/ */
historyLoaded: boolean; historyLoaded: boolean;
@@ -51,7 +46,6 @@ export const initialState: ChatState = {
messages: [], messages: [],
isReplyingAI: false, isReplyingAI: false,
wsConnected: false, wsConnected: false,
isVip: false,
guestRemainingQuota: 0, guestRemainingQuota: 0,
guestTotalQuota: 0, guestTotalQuota: 0,
quotaExceededTrigger: 0, quotaExceededTrigger: 0,