refactor: auto-init guest chat quota on first read, clean up logs
- Remove unused `quotaWarningThreshold` and `warningThreshold` getter from `GuestChatQuota` DTO - Apply get-or-init pattern in `ChatStorage.getGuestDailyChatQuota` and `getGuestTotalQuota`: when no existing value, initialize with DTO defaults (`threshold` = max per day, `totalQuotaDefault`) and return the seeded data - Fall back to original `null` result if initialization write fails - Consolidate logging strategy: drop per-actor trace logs in `chat-machine.ts`, keep business-decision logs in 3 assign actions, move all actor ENTRY/API/DONE chains to `chat-machine.actors.ts` and helper load logs to `chat-machine.helpers.ts`
This commit is contained in:
@@ -11,9 +11,7 @@ export class GuestChatQuota {
|
||||
// 静态常量
|
||||
static readonly maxQuotaPerDay = 40;
|
||||
static readonly maxQuotaPerDayTest = 4;
|
||||
static readonly quotaWarningThreshold = 20;
|
||||
|
||||
static readonly quotaWarningThresholdTest = 2;
|
||||
static readonly defaultTotalQuota = 50;
|
||||
static readonly defaultTotalQuotaDev = 5;
|
||||
static readonly quotaExhausted = 0;
|
||||
@@ -49,15 +47,6 @@ export class GuestChatQuota {
|
||||
return process.env.NODE_ENV !== "production";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前环境的配额警告阈值
|
||||
*/
|
||||
static get warningThreshold(): number {
|
||||
return GuestChatQuota.isDevelopment
|
||||
? GuestChatQuota.quotaWarningThresholdTest
|
||||
: GuestChatQuota.quotaWarningThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前环境的每日最大配额
|
||||
*/
|
||||
|
||||
@@ -18,9 +18,11 @@ import {
|
||||
GuestChatQuotaSchema,
|
||||
type GuestChatQuotaData,
|
||||
} from "@/data/schemas/chat/guest_chat_quota";
|
||||
import { GuestChatQuota as GuestChatQuotaDto } from "@/data/dto/chat/guest_chat_quota";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type Result as ResultT } from "@/utils/result";
|
||||
import { type Result as ResultT, Result } from "@/utils/result";
|
||||
import { formatDate } from "@/utils/date";
|
||||
import { SpAsyncUtil } from "@/utils/storage";
|
||||
import { StorageKeys } from "../storage_keys";
|
||||
import type { IChatStorage } from "./ichat_storage";
|
||||
@@ -41,7 +43,27 @@ export class ChatStorage implements IChatStorage {
|
||||
// ---- guest daily chat quota ----
|
||||
|
||||
getGuestDailyChatQuota(): Promise<ResultT<GuestChatQuotaData | null>> {
|
||||
return SpAsyncUtil.getJson(StorageKeys.guestChatQuota, GuestChatQuotaSchema);
|
||||
// get-or-init:**有**返**有**,**无**则**用** DTO 默认**值**(`threshold` = max per day)**初**始**化**并**返**回**
|
||||
return SpAsyncUtil.getJson(
|
||||
StorageKeys.guestChatQuota,
|
||||
GuestChatQuotaSchema,
|
||||
).then(async (existing) => {
|
||||
if (existing.success && existing.data != null) {
|
||||
return existing;
|
||||
}
|
||||
const today = formatDate(new Date());
|
||||
const initResult = await this.setGuestDailyChatQuota(
|
||||
GuestChatQuotaDto.threshold,
|
||||
today,
|
||||
);
|
||||
if (Result.isErr(initResult)) {
|
||||
return existing; // **初**始**化**失**败** → **返**原**始** null
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: { remaining: GuestChatQuotaDto.threshold, date: today },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
setGuestDailyChatQuota(remaining: number, date: string): Promise<ResultT<void>> {
|
||||
@@ -56,7 +78,22 @@ export class ChatStorage implements IChatStorage {
|
||||
// ---- guest total quota ----
|
||||
|
||||
getGuestTotalQuota(): Promise<ResultT<number | null>> {
|
||||
return SpAsyncUtil.getJson(StorageKeys.guestTotalQuotaRemaining, z.number());
|
||||
// get-or-init:**有**返**有**,**无**则**用** DTO 默认**值**(`totalQuotaDefault`)**初**始**化**并**返**回**
|
||||
return SpAsyncUtil.getJson(
|
||||
StorageKeys.guestTotalQuotaRemaining,
|
||||
z.number(),
|
||||
).then(async (existing) => {
|
||||
if (existing.success && existing.data != null) {
|
||||
return existing;
|
||||
}
|
||||
const initResult = await this.setGuestTotalQuota(
|
||||
GuestChatQuotaDto.totalQuotaDefault,
|
||||
);
|
||||
if (Result.isErr(initResult)) {
|
||||
return existing;
|
||||
}
|
||||
return { success: true, data: GuestChatQuotaDto.totalQuotaDefault };
|
||||
});
|
||||
}
|
||||
|
||||
setGuestTotalQuota(remaining: number): Promise<ResultT<void>> {
|
||||
|
||||
+25
-131
@@ -47,9 +47,11 @@
|
||||
* - `chat-machine.helpers.ts` —— 纯函数 + 数据加载(readGuestQuota + readAndSyncHistory)
|
||||
* - `chat-machine.actors.ts` —— 5 个 XState actor(loadQuota + loadHistory + sendMessage + loadMoreHistory + chatWebSocket)
|
||||
*
|
||||
* **调试日志**(保留 —— 用户要求"不要将其删除"):
|
||||
* - 命名约定:`[<file>]` 前缀标识来源
|
||||
* - 4 actor **全链日志**(ENTRY / API / DONE / 关键决策)—— 排查 actor **没**被调时**一眼**看到
|
||||
* **日**志**策略**(**清**理**后**):
|
||||
* - **不**在**状**态**机**文**件**中**打**追踪**日**志**(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";
|
||||
@@ -95,9 +97,11 @@ export const chatMachine = setup({
|
||||
actions: {
|
||||
appendUserMessage: assign(({ context, event }) => {
|
||||
if (event.type !== "ChatSendMessage") return {};
|
||||
|
||||
const newQuota = context.wsConnected
|
||||
? context.guestRemainingQuota
|
||||
: Math.max(0, context.guestRemainingQuota - 1);
|
||||
|
||||
console.log("[chat-machine] appendUserMessage", {
|
||||
contentLength: event.content.length,
|
||||
contentPreview: event.content.slice(0, 50),
|
||||
@@ -109,6 +113,7 @@ export const chatMachine = setup({
|
||||
newQuota,
|
||||
isReplyingAI: true,
|
||||
});
|
||||
|
||||
return {
|
||||
messages: [
|
||||
...context.messages,
|
||||
@@ -195,26 +200,6 @@ export const chatMachine = setup({
|
||||
|
||||
setWsConnected: assign({ wsConnected: true }),
|
||||
clearWsConnected: assign({ wsConnected: false }),
|
||||
|
||||
logSendMessageReceived: ({ event, context }) =>
|
||||
console.log("[chat-machine] SendMessage received", {
|
||||
contentLength: event.type === "ChatSendMessage" ? event.content.length : 0,
|
||||
isReplyingAI: context.isReplyingAI,
|
||||
wsConnected: context.wsConnected,
|
||||
currentMessagesCount: context.messages.length,
|
||||
}),
|
||||
|
||||
logSendImageReceived: ({ event, context }) =>
|
||||
console.log("[chat-machine] SendImage received", {
|
||||
hasImageBase64: event.type === "ChatSendImage" ? !!event.imageBase64 : false,
|
||||
wsConnected: context.wsConnected,
|
||||
}),
|
||||
|
||||
logLoadMoreHistoryReceived: ({ context }) =>
|
||||
console.log("[chat-machine] LoadMoreHistory received", {
|
||||
currentOffset: context.historyOffset,
|
||||
hasMore: context.hasMore,
|
||||
}),
|
||||
},
|
||||
}).createMachine({
|
||||
id: "chat",
|
||||
@@ -248,17 +233,11 @@ export const chatMachine = setup({
|
||||
id: "loadQuota",
|
||||
src: "loadQuota",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => {
|
||||
console.log("[chat-machine] guestSession.initializing.loadQuota.onDone", {
|
||||
remaining: event.output.remaining,
|
||||
total: event.output.total,
|
||||
});
|
||||
return {
|
||||
actions: assign(({ event }) => ({
|
||||
guestRemainingQuota: event.output.remaining,
|
||||
guestTotalQuota: event.output.total,
|
||||
quotaLoaded: true,
|
||||
};
|
||||
}),
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
// 失败**也**标 loaded,**不**卡 init(**游**客**可**能 quota 服务**不**可**用**,**让** UI **进** ready)
|
||||
@@ -272,23 +251,12 @@ export const chatMachine = setup({
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => {
|
||||
console.log(
|
||||
"[chat-machine] guestSession.initializing.loadHistory.onDone",
|
||||
{
|
||||
finalCount: event.output.messages.length,
|
||||
hasMore: event.output.hasMore,
|
||||
newOffset: event.output.newOffset,
|
||||
localOverwritten: event.output.localOverwritten,
|
||||
},
|
||||
);
|
||||
return {
|
||||
actions: assign(({ event }) => ({
|
||||
messages: event.output.messages,
|
||||
hasMore: event.output.hasMore,
|
||||
historyOffset: event.output.newOffset,
|
||||
historyLoaded: true,
|
||||
};
|
||||
}),
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
// 失败**也**标 loaded,**不**卡 init(**让** UI **还**是**能**进** ready,**屏**幕**可**能**空**)
|
||||
@@ -316,9 +284,9 @@ export const chatMachine = setup({
|
||||
guard: ({ context }) => context.guestRemainingQuota <= 0,
|
||||
actions: "incrementQuotaExceeded",
|
||||
},
|
||||
// 3. 两**个**配**额**都** OK + **内**容**非**空** → **发**送**
|
||||
// 3. 两**个**配**额**都** OK + **内**容**非**空** → **发**送**(**只**用** appendUserMessage **自**己**的**日**志**)
|
||||
{
|
||||
actions: ["logSendMessageReceived", "appendUserMessage"],
|
||||
actions: "appendUserMessage",
|
||||
guard: ({ event }) => event.content.trim().length > 0,
|
||||
target: "sending",
|
||||
},
|
||||
@@ -335,7 +303,7 @@ export const chatMachine = setup({
|
||||
actions: "incrementQuotaExceeded",
|
||||
},
|
||||
{
|
||||
actions: ["logSendImageReceived", "appendUserImage"],
|
||||
actions: "appendUserImage",
|
||||
target: "sending",
|
||||
},
|
||||
],
|
||||
@@ -349,11 +317,6 @@ export const chatMachine = setup({
|
||||
},
|
||||
},
|
||||
sending: {
|
||||
entry: ({ event }) =>
|
||||
console.log("[chat-machine] → guestSession.sending", {
|
||||
contentLength: event.type === "ChatSendMessage" ? event.content.length : 0,
|
||||
contentPreview: event.type === "ChatSendMessage" ? event.content.slice(0, 50) : "",
|
||||
}),
|
||||
invoke: {
|
||||
src: "sendMessageHttp",
|
||||
input: ({ event }) => ({
|
||||
@@ -361,28 +324,14 @@ export const chatMachine = setup({
|
||||
}),
|
||||
onDone: {
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign(({ context, event }) => ({
|
||||
actions: assign(({ context, event }) => ({
|
||||
messages: [...context.messages, event.output.reply],
|
||||
isReplyingAI: false,
|
||||
})),
|
||||
({ context, event }) =>
|
||||
console.log("[chat-machine] guestSession.sending.onDone", {
|
||||
newReplyContentLength: event.output.reply.content.length,
|
||||
totalMessagesCount: context.messages.length + 1,
|
||||
isReplyingAI: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign({ isReplyingAI: false }),
|
||||
({ event }) =>
|
||||
console.error("[chat-machine] guestSession.sending.onError", {
|
||||
error: event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
],
|
||||
actions: assign({ isReplyingAI: false }),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -416,24 +365,13 @@ export const chatMachine = setup({
|
||||
// 任务 2:拉 history(**只**调**用** loadHistoryActor,**不**再**用** loadMoreHistoryActor)
|
||||
src: "loadHistory",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => {
|
||||
console.log(
|
||||
"[chat-machine] userSession.initializing.loadHistory.onDone",
|
||||
{
|
||||
finalCount: event.output.messages.length,
|
||||
hasMore: event.output.hasMore,
|
||||
newOffset: event.output.newOffset,
|
||||
localOverwritten: event.output.localOverwritten,
|
||||
},
|
||||
);
|
||||
return {
|
||||
actions: assign(({ event }) => ({
|
||||
messages: event.output.messages,
|
||||
isLoadingMore: false,
|
||||
hasMore: event.output.hasMore,
|
||||
historyOffset: event.output.newOffset,
|
||||
historyLoaded: true,
|
||||
};
|
||||
}),
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
// 失败**也**标 loaded,**不**卡 init
|
||||
@@ -447,22 +385,14 @@ export const chatMachine = setup({
|
||||
ready: {
|
||||
on: {
|
||||
ChatSendMessage: {
|
||||
actions: ["logSendMessageReceived", "appendUserMessage"],
|
||||
actions: "appendUserMessage",
|
||||
guard: ({ event }) => event.content.trim().length > 0,
|
||||
target: "sending",
|
||||
},
|
||||
ChatSendImage: {
|
||||
actions: ["logSendImageReceived", "appendUserImage"],
|
||||
actions: "appendUserImage",
|
||||
},
|
||||
ChatLoadMoreHistory: {
|
||||
actions: [
|
||||
"logLoadMoreHistoryReceived",
|
||||
({ context }) =>
|
||||
console.log("[chat-machine] userSession.ready.ChatLoadMoreHistory received", {
|
||||
currentOffset: context.historyOffset,
|
||||
hasMore: context.hasMore,
|
||||
}),
|
||||
],
|
||||
target: "loadingMore",
|
||||
},
|
||||
ChatLogout: "#chat.idle",
|
||||
@@ -474,12 +404,6 @@ export const chatMachine = setup({
|
||||
},
|
||||
},
|
||||
sending: {
|
||||
entry: ({ event }) =>
|
||||
console.log("[chat-machine] → userSession.sending", {
|
||||
contentLength: event.type === "ChatSendMessage" ? event.content.length : 0,
|
||||
contentPreview: event.type === "ChatSendMessage" ? event.content.slice(0, 50) : "",
|
||||
wsConnected: true,
|
||||
}),
|
||||
invoke: {
|
||||
src: "sendMessageHttp",
|
||||
input: ({ event }) => ({
|
||||
@@ -487,63 +411,33 @@ export const chatMachine = setup({
|
||||
}),
|
||||
onDone: {
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign(({ context, event }) => ({
|
||||
actions: assign(({ context, event }) => ({
|
||||
messages: [...context.messages, event.output.reply],
|
||||
isReplyingAI: false,
|
||||
})),
|
||||
({ context, event }) =>
|
||||
console.log("[chat-machine] userSession.sending.onDone", {
|
||||
newReplyContentLength: event.output.reply.content.length,
|
||||
totalMessagesCount: context.messages.length + 1,
|
||||
isReplyingAI: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign({ isReplyingAI: false }),
|
||||
({ event }) =>
|
||||
console.error("[chat-machine] userSession.sending.onError", {
|
||||
error: event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
],
|
||||
actions: assign({ isReplyingAI: false }),
|
||||
},
|
||||
},
|
||||
},
|
||||
loadingMore: {
|
||||
entry: ({ context }) =>
|
||||
console.log("[chat-machine] → userSession.loadingMore", { offset: context.historyOffset }),
|
||||
invoke: {
|
||||
src: "loadMoreHistory",
|
||||
input: ({ context }) => ({ offset: context.historyOffset }),
|
||||
onDone: {
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign(({ context, event }) => ({
|
||||
actions: assign(({ context, event }) => ({
|
||||
messages: [...event.output.messages, ...context.messages],
|
||||
isLoadingMore: false,
|
||||
hasMore: event.output.hasMore,
|
||||
historyOffset: event.output.newOffset,
|
||||
})),
|
||||
({ event }) =>
|
||||
console.log("[chat-machine] userSession.loadingMore.onDone", {
|
||||
newMessagesCount: event.output.messages.length,
|
||||
newOffset: event.output.newOffset,
|
||||
hasMore: event.output.hasMore,
|
||||
}),
|
||||
],
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: [
|
||||
assign({ isLoadingMore: false }),
|
||||
({ event }) =>
|
||||
console.error("[chat-machine] userSession.loadingMore.onError", {
|
||||
error: event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
],
|
||||
actions: assign({ isLoadingMore: false }),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,8 +29,6 @@ export interface ChatState {
|
||||
*/
|
||||
wsConnected: boolean;
|
||||
/** 游客剩余配额(次/天)—— **仅**游客业务展示用
|
||||
* - default 0 = "未初始化" / "已耗尽"(同码,靠 `isGuest` 区分)
|
||||
* - 非游客:永不被赋值(业务事实"无消息限制")
|
||||
*/
|
||||
guestRemainingQuota: number;
|
||||
/** 游客总配额(仅游客展示用) */
|
||||
|
||||
Reference in New Issue
Block a user