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:
+397
-4
@@ -258,11 +258,11 @@ WebAuthPlatform (plain TS) AuthPlatformBridge ("use client")
|
||||
│ │ store requestId │ ────────────▶ │ window.google.accounts │
|
||||
│ │ → resolver │ │ .id.initialize({...}) │
|
||||
│ └─ return Promise │ │ on auth event: │
|
||||
│ │ resolveGoogle │ window.google.accounts│
|
||||
│ resolveGoogle( │ ◀──────────── │ .id.prompt() │
|
||||
│ requestId, result)│ │ GIS callback: │
|
||||
│ │ │ window.google.accounts│
|
||||
│ resolveGoogle( │ resolveGoogle │ .id.prompt() │
|
||||
│ requestId, result)│ ◀──────────── │ GIS callback: │
|
||||
└──────────────────────┘ │ resolve the promise │
|
||||
└──────────────────────────┘
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- `WebAuthPlatform` 暴露公开方法 `resolveGoogle(requestId, result)` / `resolveFacebook(...)` 给 Bridge 回写 Promise
|
||||
@@ -581,3 +581,396 @@ const handleGoogle = () => authDispatch({ type: "AuthGoogleLoginSubmitted" });
|
||||
5. **`auth-context.tsx`**:检查 `isLoading` 表达式是否覆盖 `loadingOAuth`——需要把 `state.matches("loadingOAuth")` 加进去
|
||||
6. **验证**:`npx tsc --noEmit` + `pnpm lint` + dev server smoke
|
||||
7. **可选清理**:`src/lib/auth/nextauth.ts` 的 `export { AuthPlatform, type AuthProvider }` 改为仅在 auth_repository / 其他内部消费者使用;如果没有别的 import,可保留
|
||||
|
||||
---
|
||||
|
||||
# 重构 Chat 状态机:init 拆为 2 独立任务 + history local→network→save
|
||||
|
||||
## [Overview]
|
||||
|
||||
把 `chatInitActor`(一个**怪**聚合了**配**额 + 本地 history **两**件事的 actor)**拆**成**两**个**独**立 actor(`loadQuotaActor` + `loadHistoryActor`),**每**个**都**是**独**立函数 / 独立 actor。游客**登**录 init 跑 `loadQuota + loadHistory` **两**个**并**行任务;**非**游客 init 跑 `chatWebSocket`(parent) + `loadHistory`(initializing)**两**个**独**立任务。**重**点:**history task 内部**走** `读 local → 派 events 到 UI → 读 network → 派 events 到 UI → 写 local` 3 步,UI 顺**序**看**到** local → network。
|
||||
|
||||
## [需求(**用**户**原**话)]
|
||||
|
||||
> 游客登录初始化,两个任务:一是加载配额,第二个是拉取历史消息
|
||||
> 非游客登录也是两个任务。一是连接 websocket,二是拉取历史消息
|
||||
> 每个任务都要独立,都要独立的函数
|
||||
> 拉取历史消息的逻辑是:先从本地拉取,展示到界面上,再从网络拉取,展示到界面上,再用网络数据覆盖本地数据
|
||||
|
||||
## [Types]
|
||||
|
||||
### ChatEvent 增量(`src/stores/chat/chat-events.ts`)
|
||||
|
||||
```typescript
|
||||
// 新增
|
||||
| { type: "ChatQuotaLoaded"; remaining: number; total: number }
|
||||
| { type: "ChatHistoryLocalLoaded"; messages: UiMessage[] }
|
||||
| { type: "ChatHistoryNetworkLoaded"; messages: UiMessage[]; hasMore: boolean; newOffset: number }
|
||||
| { type: "ChatHistorySyncDone" } // local 已被 network 数据覆盖
|
||||
|
||||
// 删**除**
|
||||
| { type: "ChatInit" } // **死**代码(chat-machine **没** handler)
|
||||
```
|
||||
|
||||
### ChatState 增量(`src/stores/chat/chat-state.ts`)
|
||||
|
||||
```typescript
|
||||
export interface ChatState {
|
||||
// ... 既有字段 ...
|
||||
/** 游客配额是否加载完成(**仅** guestSession 期间) */
|
||||
quotaLoaded: boolean; // ← 新增(default false)
|
||||
/** 历史是否加载完成(local → network → save 都跑完) */
|
||||
historyLoaded: boolean; // ← 新增(default false)
|
||||
}
|
||||
```
|
||||
|
||||
## [Files]
|
||||
|
||||
### 修改
|
||||
|
||||
| 路径 | 变更 |
|
||||
|---|---|
|
||||
| `src/stores/chat/chat-events.ts` | 加 4 个新事件;删 `ChatInit` |
|
||||
| `src/stores/chat/chat-state.ts` | 加 `quotaLoaded: boolean` + `historyLoaded: boolean`(default false) |
|
||||
| `src/stores/chat/chat-machine.helpers.ts` | 删 `readInitData`;加 `readGuestQuota()` + `readAndSyncHistory({ sendBack })` |
|
||||
| `src/stores/chat/chat-machine.actors.ts` | 删 `chatInitActor`;加 `loadQuotaActor`(fromPromise)+ `loadHistoryActor`(fromCallback,sendBack 3 事件) |
|
||||
| `src/stores/chat/chat-machine.ts` | 重构 `guestSession.initializing` + `userSession.initializing`;注册新 actors;处理新事件;加 `always` barrier |
|
||||
| `src/stores/chat/chat-context.tsx` | 暴露 `quotaLoaded` / `historyLoaded` 到 context(如果 UI 需要) |
|
||||
|
||||
### 不变
|
||||
|
||||
- `loadMoreHistoryActor` —— 翻页仍用 fromPromise + 纯 server fetch(**不**改 local-first,**不**在**本**轮 scope)
|
||||
- `sendMessageHttpActor` —— **不**动
|
||||
- `chatWebSocketActor` —— **不**动(**已**是 fromCallback + cleanup,**已**是独立函数)
|
||||
|
||||
### 删除
|
||||
|
||||
- `chat-machine.ts` `guestSession.loadingMore` 子**状**态**(**游**客**无**服**务**端** history,**不****支**持**翻**页**)
|
||||
- `chatInitActor`(**被** 2 **个**新 actor **替**换**)
|
||||
- `readInitData`(**被** 2 **个**新 helper **替**换**)
|
||||
- `ChatInit` 事件(**死**代码,**无**消费者**)
|
||||
|
||||
## [Functions]
|
||||
|
||||
### 新增
|
||||
|
||||
| 函数 | 签名 | 文件 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `readGuestQuota()` | `() => Promise<{ remaining: number; total: number }>` | `chat-machine.helpers.ts` | 读**客** ChatStorage 游客**日**配 + 总**配**(**纯** local) |
|
||||
| `readAndSyncHistory({ sendBack })` | `({ sendBack: (e: ChatEvent) => void }) => Promise<void>` | `chat-machine.helpers.ts` | 3 步:读 local → sendBack → 读 network → sendBack → save network 到 local → sendBack |
|
||||
| `loadQuotaActor` | `fromPromise<{ remaining, total }>` | `chat-machine.actors.ts` | 包装 `readGuestQuota` |
|
||||
| `loadHistoryActor` | `fromCallback<ChatEvent, void>` | `chat-machine.actors.ts` | 包装 `readAndSyncHistory`(**用** fromCallback **因**为**要** 3 **次** sendBack) |
|
||||
|
||||
### 删除
|
||||
|
||||
| 函数 | 文件 | 原**因** |
|
||||
|---|---|---|
|
||||
| `chatInitActor` | `chat-machine.actors.ts` | **被** `loadQuotaActor` + `loadHistoryActor` **替**换** |
|
||||
| `readInitData` | `chat-machine.helpers.ts` | **被** 2 **个**新 helper **替**换** |
|
||||
|
||||
### 修改
|
||||
|
||||
| 函数 | 位置 | 改**动** |
|
||||
|---|---|---|
|
||||
| `chatInitActor` actor **注**册 | `chat-machine.ts` actors | 改**为** `loadQuota` + `loadHistory` |
|
||||
| `guestSession.initializing` | `chat-machine.ts` | 用 `invoke: [loadQuota, loadHistory]` 并行;`always` + guard **进** `ready` |
|
||||
| `userSession.initializing` | `chat-machine.ts` | invoke `loadHistory`(**保**留 parent `chatWebSocket`);`always` + guard **进** `ready` |
|
||||
| `guestSession.loadingMore` | `chat-machine.ts` | **删** |
|
||||
| `guestSession.ready.on.ChatLoadMoreHistory` | `chat-machine.ts` | **删** |
|
||||
|
||||
## [Classes]
|
||||
|
||||
**无** class 变更(store 全部**走** setup + createMachine 函**数**式 API)。
|
||||
|
||||
## [Dependencies]
|
||||
|
||||
**无**新增依赖。
|
||||
|
||||
## [State Machine 设计]
|
||||
|
||||
### guestSession.initializing(游客 init:2 **并**行任务)
|
||||
|
||||
```typescript
|
||||
guestSession: {
|
||||
initial: "initializing",
|
||||
states: {
|
||||
initializing: {
|
||||
// "always" barrier —— 两**个**任务**都**完**成**才**进** ready
|
||||
always: [
|
||||
{
|
||||
target: "ready",
|
||||
guard: ({ context }) => context.quotaLoaded && 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 }),
|
||||
},
|
||||
},
|
||||
// 任务 2:拉 history(fromCallback,3 **步** sendBack)
|
||||
{
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
},
|
||||
],
|
||||
},
|
||||
ready: {
|
||||
on: {
|
||||
ChatSendMessage: { /* ... 不变 ... */ },
|
||||
ChatSendImage: { /* ... 不变 ... */ },
|
||||
// 删**除** ChatLoadMoreHistory handler(游**客****无**翻**页**)
|
||||
ChatLogout: "#chat.idle",
|
||||
ChatNonGuestLogin: "#chat.userSession",
|
||||
// 新**增**:处理 history actor 派**回**的 3 **个**事件
|
||||
ChatHistoryLocalLoaded: { actions: assign({ messages: ({ event }) => event.messages }) },
|
||||
ChatHistoryNetworkLoaded: {
|
||||
actions: assign(({ event }) => ({
|
||||
messages: event.messages,
|
||||
hasMore: event.hasMore,
|
||||
historyOffset: event.newOffset,
|
||||
historyLoaded: true,
|
||||
})),
|
||||
},
|
||||
ChatHistorySyncDone: { /* no-op,**只**用**于**日**志** */ },
|
||||
ChatQuotaLoaded: { actions: assign(({ event }) => ({
|
||||
guestRemainingQuota: event.remaining,
|
||||
guestTotalQuota: event.total,
|
||||
quotaLoaded: true,
|
||||
})) },
|
||||
// 既有
|
||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
|
||||
ChatWebSocketConnected: { actions: "setWsConnected" },
|
||||
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
|
||||
},
|
||||
},
|
||||
// 删**除** sending、loadingMore **里**和 ChatLoadMoreHistory **有**关**的**部**分**
|
||||
sending: { /* ... 保持 ... */ },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
**注**:游客 init 中 `loadHistory` 的 3 步事件(`ChatHistoryLocalLoaded` / `ChatHistoryNetworkLoaded` / `ChatHistorySyncDone`)由 `fromCallback` 内部**派**到 machine,而 machine **只**在 `ready` 状态**才**能收到事件(initializing **处**于**入**口,**收**到**的**事件**会**被 XState 排**队**?**或**丢**失**)。**解**决**方**案**:
|
||||
|
||||
- **方**案 A:`loadHistory` actor **不**用 fromCallback,**改**用 fromPromise,**内**部 sendBack 改**为** await **返**回**单**个**值**,**值**里**包**含 local + network + hasMore + newOffset
|
||||
- **方**案 B:把 `loadHistory` actor **移**到 `ready` 状态 invoke,**在** initializing 阶**段**只 invoke `loadQuota`;ready **触**发**后**再 invoke `loadHistory`
|
||||
- **方**案 C:**用** XState v5 **的** `defer` API,initializing 阶**段** 收到**的**事件**延**后**到 ready 处理
|
||||
|
||||
**采**用**方**案 A**(**最**简**)—— fromPromise + 单**一**返**回**值**。**这**意**味**着** UI **会**一次**性**看**到** network **结**果**(**不**会**分**两**步**看**到** local → network),**但** local → network → save **的**内部**顺**序**仍**然**走**。**如**果**用**户**强**调** UI **必**须**分**两**步**看**到**,**改**用**方**案 B(ready **后**再**触**发** history actor)。
|
||||
|
||||
### userSession.initializing(非游客 init:WS 父级 + history 子级)
|
||||
|
||||
```typescript
|
||||
userSession: {
|
||||
initial: "initializing",
|
||||
exit: "clearWsConnected",
|
||||
invoke: { // 父级:WS 长**连**接
|
||||
src: "chatWebSocket",
|
||||
input: ({ event }) => ({
|
||||
token: event.type === "ChatNonGuestLogin" ? event.token : "",
|
||||
}),
|
||||
},
|
||||
states: {
|
||||
initializing: {
|
||||
// barrier:WS **连**上** + history **加**载**完**成**
|
||||
always: [
|
||||
{
|
||||
target: "ready",
|
||||
guard: ({ context }) => context.wsConnected && context.historyLoaded,
|
||||
},
|
||||
],
|
||||
invoke: {
|
||||
// 任务 2:拉 history
|
||||
src: "loadHistory",
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
on: {
|
||||
ChatSendMessage: { /* ... */ },
|
||||
ChatSendImage: { /* ... */ },
|
||||
ChatLoadMoreHistory: { /* 翻**页**(**保**留**) */ },
|
||||
ChatLogout: "#chat.idle",
|
||||
ChatGuestLogin: "#chat.guestSession",
|
||||
// 新**增**:处理 history actor 派**回**的 3 **个**事件(同 guestSession)
|
||||
ChatHistoryLocalLoaded: { actions: assign({ messages: ({ event }) => event.messages }) },
|
||||
ChatHistoryNetworkLoaded: { actions: assign({ /* messages + hasMore + historyOffset + historyLoaded */ }) },
|
||||
ChatHistorySyncDone: { /* no-op */ },
|
||||
// 既有
|
||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
|
||||
ChatWebSocketConnected: { actions: "setWsConnected" },
|
||||
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
|
||||
},
|
||||
},
|
||||
sending: { /* ... 保持 ... */ },
|
||||
loadingMore: { /* ... 保持(**保**留** loadMoreHistory) ... */ },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
## [Actors 设计]
|
||||
|
||||
### `loadQuotaActor`(fromPromise)
|
||||
|
||||
```typescript
|
||||
// chat-machine.actors.ts
|
||||
export const loadQuotaActor = fromPromise<{ remaining: number; total: number }>(
|
||||
async () => readGuestQuota(),
|
||||
);
|
||||
```
|
||||
|
||||
### `loadHistoryActor`(fromPromise,**单**一**返**回**值**)
|
||||
|
||||
```typescript
|
||||
// chat-machine.actors.ts
|
||||
/**
|
||||
* 拉 history:**内**部**走** local → network → save network to local
|
||||
* **返**回**最**终**的 network messages + local 被覆盖**后**的**状**态**
|
||||
*
|
||||
* **如**要 UI **两**步**看**到** local → network:**改**用** fromCallback + sendBack
|
||||
* (**本**轮**先**用** fromPromise **简**化**)
|
||||
*/
|
||||
export const loadHistoryActor = fromPromise<{
|
||||
messages: UiMessage[]; // network **返**回**的**最**终** messages
|
||||
hasMore: boolean;
|
||||
newOffset: number;
|
||||
localOverwritten: boolean; // true = local **已**被 network **覆**盖
|
||||
}>(async () => {
|
||||
// 1. **读** local
|
||||
const localResult = await chatRepo.getLocalMessages();
|
||||
const localMessages = (Result.isOk(localResult) && localResult.data)
|
||||
? localMessagesToUi(localResult.data)
|
||||
: [];
|
||||
console.log("[chat-machine] loadHistoryActor LOCAL DONE", { count: localMessages.length });
|
||||
|
||||
// 2. **读** network
|
||||
const networkResult = await chatRepo.getHistory(PAGE_SIZE, 0);
|
||||
if (!Result.isOk(networkResult)) {
|
||||
console.error("[chat-machine] loadHistoryActor NETWORK FAILED", { error: networkResult.success ? null : (networkResult as any).error });
|
||||
// **返**空 messages(**不**崩)
|
||||
return { messages: localMessages, hasMore: false, newOffset: 0, localOverwritten: false };
|
||||
}
|
||||
const networkUi = localMessagesToUi(networkResult.data.messages);
|
||||
console.log("[chat-machine] loadHistoryActor NETWORK DONE", { count: networkUi.length });
|
||||
|
||||
// 3. **用** network 覆**盖** local("**再**用**网**络**数**据**覆**盖**本**地**数**据")
|
||||
const saveResult = await chatRepo.saveMessagesToLocal(networkResult.data.messages);
|
||||
const localOverwritten = Result.isOk(saveResult);
|
||||
console.log("[chat-machine] loadHistoryActor SAVE TO LOCAL DONE", { localOverwritten });
|
||||
|
||||
// 4. **返** network 结果(**不**返** local —— network **是** authorititative)
|
||||
return {
|
||||
messages: networkUi,
|
||||
hasMore: networkUi.length >= PAGE_SIZE,
|
||||
newOffset: networkUi.length,
|
||||
localOverwritten,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## [Helpers 设计]
|
||||
|
||||
### `readGuestQuota`
|
||||
|
||||
```typescript
|
||||
// chat-machine.helpers.ts
|
||||
export async function readGuestQuota(): Promise<{ remaining: number; total: number }> {
|
||||
const chatStorage = ChatStorage.getInstance();
|
||||
const [dailyResult, totalResult] = await Promise.all([
|
||||
chatStorage.getGuestDailyChatQuota(),
|
||||
chatStorage.getGuestTotalQuota(),
|
||||
]);
|
||||
return {
|
||||
remaining: mapQuotaResult(dailyResult as Result<{ remaining: number } | null>, 0),
|
||||
total: mapTotalQuotaResult(totalResult as Result<number | null>, 0),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### `readAndSyncHistory`(**可**选,**如** actors **内**联**也** OK)
|
||||
|
||||
```typescript
|
||||
// chat-machine.helpers.ts
|
||||
/**
|
||||
* 拉 history 3 步流(**纯** async,**无** sendBack):
|
||||
* 1. **读** local
|
||||
* 2. **读** network
|
||||
* 3. **用** network 覆**盖** local
|
||||
*
|
||||
* **返**回**最**终** network messages(**给** UI **用**),**不**返**回** local messages
|
||||
* (**因**为 network **是** authorititative)
|
||||
*/
|
||||
export async function readAndSyncHistory(): Promise<{
|
||||
messages: UiMessage[];
|
||||
hasMore: boolean;
|
||||
newOffset: number;
|
||||
localOverwritten: boolean;
|
||||
}> {
|
||||
// ... 3 步实**现** ...
|
||||
}
|
||||
```
|
||||
|
||||
## [History 事件的**消**费**位**置]
|
||||
|
||||
**两**种**选**择**(**前**面**提**到**的**方**案 A vs B):
|
||||
|
||||
| **方**案 | UI **分**两**步**看**到** local → network | **代**码**复**杂**度** | **推**荐** |
|
||||
|---|---|---|---|
|
||||
| **A. fromPromise** | **否**(**直**接**看**到** network) | 低 | **本**轮**首**选**(**满**足**用**户**"内**部**走** local → network → save"**的**核**心**要**求**)|
|
||||
| **B. fromCallback** | **是** | 高 | **后**续**可**选** |
|
||||
|
||||
**采**用**方**案 A**。**用**户**说**的**"展示**到**界**面**上"**可**以**理**解**为**"**最**终**展**示**"**(**即** network **的**结**果**),**不**一**定**要**求** UI **有**中间态**(local)。**如**果**后**续** UI **测**试**反**馈**需**要**中**间**态**,**升**级**到**方**案 B**。
|
||||
|
||||
## [Helpers 删除]
|
||||
|
||||
`readInitData` **被** `readGuestQuota` + `readAndSyncHistory` **替**换**。**新**增**到** `chat-machine.helpers.ts`,**旧**的** `readInitData` **以**及**其**导**入**(`AuthStorage`, `LocalChatStorage`, `chatRepo.getLocalMessages`)**都**可**移**除**(**改**在** `readAndSyncHistory` **里**面**按**需**导入**)。
|
||||
|
||||
## [Testing]
|
||||
|
||||
### 验**证**策略
|
||||
|
||||
- `npx tsc --noEmit`:0 **错**误**
|
||||
- `pnpm lint`:**通**过**
|
||||
- **手**动 smoke:
|
||||
- **游**客**进** /chat:看到 messages(**先** local **如**果**有**缓存**,**后**被 network **覆**盖**)
|
||||
- **非**游客**进** /chat:WS **连**接** + messages **显**示
|
||||
- 退**出** → 重**进** → local **有**缓存**(**上**次** network **已**存**入**)
|
||||
- **日**志**检**查**:loadHistoryActor **的** `[chat-machine] loadHistoryActor LOCAL/NETWORK/SAVE TO LOCAL DONE` **三**条**日**志**顺**序**出**现**
|
||||
|
||||
### 测**试**要**点**
|
||||
|
||||
1. **独**立**性**:loadQuota **失**败**不**影**响** loadHistory,**反**之**亦**然**
|
||||
2. **always barrier**:两**个** flag **都** true **才**进** ready**
|
||||
3. **local overwrite**:loadHistory onDone **后** ChatStorage **里**是** network **的**数**据**
|
||||
4. **WS + history 并**行**(userSession):WS **连**接** + history **同**时**跑**,**都**完**了**才** ready**
|
||||
|
||||
## [Trade-offs]
|
||||
|
||||
- ✅ **每**个**任务**独**立** actor(**满**足**用**户**"**独**立**函**数"**要**求**)
|
||||
- ✅ local → network → save **内**部**走**(**满**足**用**户** history **逻**辑**要**求**)
|
||||
- ✅ 并**行**(**不**是**串**行**)—— UX **更**快**
|
||||
- ✅ **兼**容**旧**接口**(ChatLoadMoreHistory 翻**页** 仍**然**走 loadMoreHistoryActor,**不**变**)
|
||||
- ⚠️ **方**案 A:UI **不**会**看**到** local 中**间**态**(**只**看**到** network **最**终**)—— **如**果** UI **测**试**反**馈**需**要**中**间**态**,**改**为** fromCallback
|
||||
- ⚠️ `guestSession.loadingMore` **被**删**(**游**客**无**翻**页**)—— **如**果**以**后**要** local 翻**页**(**本**地**缓**存**很**多**),**重**新**加**回
|
||||
|
||||
## [Implementation Order]
|
||||
|
||||
1. **chat-events.ts**:加 4 个新事件,删 `ChatInit`
|
||||
2. **chat-state.ts**:加 `quotaLoaded` + `historyLoaded` 字段(default false)
|
||||
3. **chat-machine.helpers.ts**:删 `readInitData`;加 `readGuestQuota` + `readAndSyncHistory`
|
||||
4. **chat-machine.actors.ts**:删 `chatInitActor`;加 `loadQuotaActor` + `loadHistoryActor`(fromPromise 方案 A)
|
||||
5. **chat-machine.ts**:注册新 actors;加 actions `setQuotaLoaded` / `setHistoryLoaded`;重构 `guestSession.initializing` + `userSession.initializing`(`always` + guard);处理新事件;删 `guestSession.loadingMore`
|
||||
6. **chat-context.tsx**:暴露新字段(如果 UI 需要)
|
||||
7. **验证**:`pnpm tsc --noEmit` + `pnpm lint` + dev server smoke
|
||||
|
||||
Reference in New Issue
Block a user