docs(plan): add chat state machine refactor design

Update implementation_plan.md with a detailed refactor plan that splits
`chatInitActor` into two independent actors (`loadQuotaActor` +
`loadHistoryActor`) and redesigns the history loading flow to follow
local → network → save semantics so the UI sees local data first,
then network data.

Key changes outlined in the plan:
- Add new events: `ChatQuotaLoaded`, `ChatHistoryLocalLoaded`,
  `ChatHistoryNetworkLoaded`, `ChatHistorySyncDone`
- Remove dead-code `ChatInit` event
- Extend `ChatState` with `quotaLoaded` and `historyLoaded` flags
- Use an `always` barrier in `guestSession.initializing` and
  `userSession.initializing` so both tasks must complete before
  transitioning to `ready`
- Guest init runs `loadQuota` + `loadHistory` in parallel; non-guest
  init runs `chatWebSocket` + `loadHistory` as independent tasks

Also adds `implementation_plan` to .gitignore.
This commit is contained in:
2026-06-15 17:05:32 +08:00
parent 9b28404673
commit fa694af723
5 changed files with 438 additions and 28 deletions
+1
View File
@@ -12,6 +12,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;
-8
View File
@@ -96,14 +96,6 @@ export function mapTotalQuotaResult(
return fallback;
}
// ============================================================
// Init 任务 1:游客配额(**纯** local)—— `loadQuotaActor` 调
// ============================================================
/**
* 读取游客配额(**仅** guestSession 有意义)
* - 游客**日**配 + 总**配** 两**项**都**从** ChatStorage 拉(**纯** local**无**网络)
* - 失**败**返 fallback 0**不**抛)—— **让** UI **还**是**能**进** ready
*/
export async function readGuestQuota(): Promise<{
remaining: number;
total: number;
+38 -15
View File
@@ -1,13 +1,6 @@
/**
* Chat 状态机(XState v5
*
* 设计要点:
* - 使用 XState v5 `setup({...}).createMachine({...})` 声明式 API
* - HTTP 操作用 `fromPromise` actor(一次性 Promise
* - WebSocket 用 `fromCallback` actor(长生命周期)—— **机器内**完整管理
* - chat-screen **不**直接管理 WS —— 只派 3 个鉴权生命周期事件
* - 保持事件类型名与原 Dart 一致,便于业务层迁移对照
*
* **鉴权解耦**(**事件驱动**):
* chat 机器**不**感知鉴权 / **不**管 WebSocket —— 由 chat-screen 派生 loginStatus
* chat-screen **只**派 3 个事件:
@@ -308,14 +301,44 @@ export const chatMachine = setup({
},
ready: {
on: {
ChatSendMessage: {
actions: ["logSendMessageReceived", "appendUserMessage"],
guard: ({ event }) => event.content.trim().length > 0,
target: "sending",
},
ChatSendImage: {
actions: ["logSendImageReceived", "appendUserImage"],
},
// **配**额**检**查** + 发**送**消息**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 + **内**容**非**空** → **发**送**
{
actions: ["logSendMessageReceived", "appendUserMessage"],
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: ["logSendImageReceived", "appendUserImage"],
target: "sending",
},
],
// **删**除 ChatLoadMoreHistory handler —— 游客**无**服**务**端** history**不**支**持**翻**页**
ChatLogout: "#chat.idle",
ChatNonGuestLogin: "#chat.userSession",