style: remove markdown bold syntax from code comments
Remove `**...**` emphasis markers from inline comments and JSDoc blocks across auth and chat components, machine mappers, and quota helpers. These are plain code comments, not rendered markdown, so the bold syntax was noise. Files touched: - src/app/auth/components/auth-screen.tsx - src/app/chat/components/chat-header.tsx - src/app/chat/components/chat-screen.tsx - chat machine mapper / quota helpers No functional or behavioral changes.
This commit is contained in:
@@ -8,8 +8,8 @@
|
||||
* 兼容性:保持原 `useChatState()` / `useChatDispatch()` API。
|
||||
* 内部将 XState 状态机快照映射回原 `ChatState` 形状。
|
||||
*
|
||||
* 注:WebSocket 长生命周期 actor **由 chat-screen.tsx 监听 auth 状态机驱动**——
|
||||
* 鉴权解耦(loginStatus 在 auth,chat 机器**不**感知鉴权)。
|
||||
* 注:WebSocket 长生命周期 actor 由 chat-screen.tsx 监听 auth 状态机驱动——
|
||||
* 鉴权解耦(loginStatus 在 auth,chat 机器不感知鉴权)。
|
||||
*/
|
||||
import {
|
||||
type Dispatch,
|
||||
@@ -26,7 +26,7 @@ import type { ChatEvent, ChatState as MachineContext } from "./chat-machine";
|
||||
/**
|
||||
* 对外暴露的 State 形状
|
||||
*
|
||||
* **isGuest 字段已删** —— 由 chat-screen 派生自 `auth.loginStatus === "guest"`。
|
||||
* isGuest 字段已删 —— 由 chat-screen 派生自 `auth.loginStatus === "guest"`。
|
||||
* 消费方(chat-screen)通过 `useAuthState()` 取 loginStatus,本地派生 isGuest。
|
||||
*/
|
||||
interface ChatState {
|
||||
@@ -40,9 +40,9 @@ interface ChatState {
|
||||
isLoadingMore: boolean;
|
||||
hasMore: boolean;
|
||||
historyOffset: number;
|
||||
/** 游客配额加载完成标**志**(**仅** guestSession 期间) —— UI 可**用**此**显**示 loading */
|
||||
/** 游客配额加载完成标志(仅 guestSession 期间) —— UI 可用此显示 loading */
|
||||
quotaLoaded: boolean;
|
||||
/** history 初始加**载**完成标**志**(local → network → save 跑**完**) —— UI 可**用**此**显**示 loading */
|
||||
/** history 初始加载完成标志(local → network → save 跑完) —— UI 可用此显示 loading */
|
||||
historyLoaded: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
*
|
||||
* 事件名与原 Dart ChatEvent 对齐。
|
||||
*
|
||||
* 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已**删**。
|
||||
* 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已删。
|
||||
* 鉴权状态变化(loginStatus)由 chat-screen 通过 `useAuthState()` 订阅,
|
||||
* chat 机器**不**感知鉴权。
|
||||
* chat 机器不感知鉴权。
|
||||
*
|
||||
* 鉴权生命周期事件(**本轮新增**):
|
||||
* - `ChatGuestLogin`:游客进入 /chat —— 拉本地消息 + 初始化配额(**不**连 WS)
|
||||
* - `ChatNonGuestLogin`:非游客进入 /chat —— 拉服务器端首屏 + **连** WS(token 在 payload)
|
||||
* 鉴权生命周期事件(本轮新增):
|
||||
* - `ChatGuestLogin`:游客进入 /chat —— 拉本地消息 + 初始化配额(不连 WS)
|
||||
* - `ChatNonGuestLogin`:非游客进入 /chat —— 拉服务器端首屏 + 连 WS(token 在 payload)
|
||||
* - `ChatLogout`:登出 / 切换用户 —— 断 WS + 清消息
|
||||
*
|
||||
* 设计:**所有**WS / 历史 / 配额相关副作用**全部**通过派发事件给 chat 机器处理,
|
||||
* chat-screen **不**直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。
|
||||
* 设计:所有WS / 历史 / 配额相关副作用全部通过派发事件给 chat 机器处理,
|
||||
* chat-screen 不直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。
|
||||
*/
|
||||
export type ChatEvent =
|
||||
// 鉴权生命周期(**用户**显式触发登录后由 chat-screen 派)
|
||||
// 鉴权生命周期(用户显式触发登录后由 chat-screen 派)
|
||||
| { type: "ChatGuestLogin" }
|
||||
| { type: "ChatNonGuestLogin"; token: string }
|
||||
| { type: "ChatLogout" }
|
||||
@@ -24,7 +24,7 @@ export type ChatEvent =
|
||||
| { type: "ChatSendMessage"; content: string }
|
||||
| { type: "ChatSendImage"; imageBase64: string }
|
||||
| { type: "ChatLoadMoreHistory" }
|
||||
// WebSocket / AI 推句(**由** chat 机器内部 actor 派回 —— "机器**自**驱动")
|
||||
// WebSocket / AI 推句(由 chat 机器内部 actor 派回 —— "机器自驱动")
|
||||
| {
|
||||
type: "ChatAISentenceReceived";
|
||||
index: number;
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
* Chat 状态机:4 个 XState actor
|
||||
*
|
||||
* 从 `chat-machine.ts` 抽出的"Actors"段:
|
||||
* - `loadQuotaActor`(fromPromise,**仅**游客有意义 —— 拉**日**配 + 总**配**)
|
||||
* - `loadHistoryActor`(fromPromise —— local → network → save network to local 3 **步**)
|
||||
* - `sendMessageHttpActor`(fromPromise,**已** wire —— "一次发送 = 一次返回")
|
||||
* - `loadMoreHistoryActor`(fromPromise —— 翻**页**,**不**走 local-first)
|
||||
* - `chatWebSocketActor`(fromCallback,长生命周期 —— **仅** userSession 期间 invoke)
|
||||
* - `loadQuotaActor`(fromPromise,仅游客有意义 —— 拉日配 + 总配)
|
||||
* - `loadHistoryActor`(fromPromise —— local → network → save network to local 3 步)
|
||||
* - `sendMessageHttpActor`(fromPromise,已 wire —— "一次发送 = 一次返回")
|
||||
* - `loadMoreHistoryActor`(fromPromise —— 翻页,不走 local-first)
|
||||
* - `chatWebSocketActor`(fromCallback,长生命周期 —— 仅 userSession 期间 invoke)
|
||||
*
|
||||
* 设计:
|
||||
* - 依赖 `chat-machine.helpers.ts`(纯函数 + 数据加载)
|
||||
* - 依赖 `createChatWebSocket`(`@/core/net/chat-websocket`)
|
||||
* - 保留**全链日志**(用户要求"不要将其删除")
|
||||
* - 保留全链日志(用户要求"不要将其删除")
|
||||
* - 命名约定:`[chat-machine]` 前缀 —— 与 `chat-machine.ts` 的日志对齐
|
||||
*/
|
||||
|
||||
@@ -31,12 +31,12 @@ import {
|
||||
import type { ChatEvent } from "./chat-events";
|
||||
|
||||
// ============================================================
|
||||
// Init 任务 1:拉游客配额(fromPromise,**纯** local)
|
||||
// Init 任务 1:拉游客配额(fromPromise,纯 local)
|
||||
// ============================================================
|
||||
/**
|
||||
* 拉游客**日**配 + 总**配**(**仅** guestSession 期间 invoke)
|
||||
* - 失**败**返 `{ remaining: 0, total: 0 }`(**不**抛,**让** UI **还**是**能**进** ready)
|
||||
* - onDone 在 state machine **里**赋 guestRemainingQuota + guestTotalQuota + 设 quotaLoaded: true
|
||||
* 拉游客日配 + 总配(仅 guestSession 期间 invoke)
|
||||
* - 失败返 `{ remaining: 0, total: 0 }`(不抛,让 UI 还是能进 ready)
|
||||
* - onDone 在 state machine 里赋 guestRemainingQuota + guestTotalQuota + 设 quotaLoaded: true
|
||||
*/
|
||||
export const loadQuotaActor = fromPromise<{
|
||||
remaining: number;
|
||||
@@ -49,13 +49,13 @@ export const loadQuotaActor = fromPromise<{
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Init 任务 2:拉 history 3 步流(fromPromise,**返**回**最**终**结**果**)
|
||||
// Init 任务 2:拉 history 3 步流(fromPromise,返回最终结果)
|
||||
// ============================================================
|
||||
/**
|
||||
* local → network → save network to local 3 步流(**返**回** network **最**终** messages**)
|
||||
* - 选**方**案 A(fromPromise):UI **不**会**看**到** local 中**间**态**,**直**接**显**示** network **结**果**
|
||||
* - 内部 3 步**走** helper `readAndSyncHistory`(**日**志**全**在** helper **里**)
|
||||
* - 失**败**返 local(**退**而**求**其**次**)
|
||||
* local → network → save network to local 3 步流(返回 network 最终 messages)
|
||||
* - 选方案 A(fromPromise):UI 不会看到 local 中间态,直接显示 network 结果
|
||||
* - 内部 3 步走 helper `readAndSyncHistory`(日志全在 helper 里)
|
||||
* - 失败返 local(退而求其次)
|
||||
*/
|
||||
export const loadHistoryActor = fromPromise<{
|
||||
messages: UiMessage[];
|
||||
@@ -81,10 +81,10 @@ export const loadHistoryActor = fromPromise<{
|
||||
// HTTP: one-shot Promise
|
||||
// ============================================================
|
||||
/**
|
||||
* HTTP 发送消息(**一次发送 = 一次返回**)
|
||||
* - 后端响应**就**是 AI 回复(`ChatSendResponse.reply: string`)
|
||||
* - **不**再调 `getLocalMessages()`(**多此一举**)
|
||||
* - 返回 `{ reply: UiMessage }` —— `sending.onDone` 把它**追加**到 `context.messages`
|
||||
* HTTP 发送消息(一次发送 = 一次返回)
|
||||
* - 后端响应就是 AI 回复(`ChatSendResponse.reply: string`)
|
||||
* - 不再调 `getLocalMessages()`(多此一举)
|
||||
* - 返回 `{ reply: UiMessage }` —— `sending.onDone` 把它追加到 `context.messages`
|
||||
*/
|
||||
export const sendMessageHttpActor = fromPromise<
|
||||
{ reply: UiMessage },
|
||||
@@ -122,7 +122,7 @@ export const sendMessageHttpActor = fromPromise<
|
||||
return { reply };
|
||||
});
|
||||
|
||||
/** 翻历史(pagination)—— **不**走 local-first,**纯** server fetch */
|
||||
/** 翻历史(pagination)—— 不走 local-first,纯 server fetch */
|
||||
export const loadMoreHistoryActor = fromPromise<
|
||||
{ messages: UiMessage[]; hasMore: boolean; newOffset: number },
|
||||
{ offset: number }
|
||||
@@ -173,13 +173,13 @@ export const loadMoreHistoryActor = fromPromise<
|
||||
// WebSocket: long-lived callback
|
||||
// ============================================================
|
||||
/**
|
||||
* 长生命周期 WebSocket(**仅** `userSession` 期间 invoke)
|
||||
* 长生命周期 WebSocket(仅 `userSession` 期间 invoke)
|
||||
* - 启动:连 WS,事件 → sendBack 派回机器
|
||||
* - 停止(cleanup):disconnect
|
||||
* - **游客**不** invoke 此 actor(业务事实"游客不连 WS")
|
||||
* - 游客不 invoke 此 actor(业务事实"游客不连 WS")
|
||||
*
|
||||
* 之所以用 fromCallback 而非 fromPromise:
|
||||
* - WS 是长连接,**不**是一次性 Promise
|
||||
* - WS 是长连接,不是一次性 Promise
|
||||
* - fromCallback 提供 `sendBack` 回调,actor 内部事件 → 机器
|
||||
* - cleanup 函数 → XState 在 parent state exit 时自动调用
|
||||
*/
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
* 从 `chat-machine.ts` 抽出的"helpers"段:
|
||||
* - 翻页大小常量
|
||||
* - 仓库注入(接口类型 → 单例)
|
||||
* - DTO ↔ UiMessage 映射(**纯函数** —— 可单测)
|
||||
* - Result → number 映射(**纯函数** —— 可单测)
|
||||
* - 2 **个**独立 init 任务**的**数据加载**:
|
||||
* - `readGuestQuota()` —— 游客**日**配 + 总**配**(**仅**游客有意义,**纯** ChatStorage)
|
||||
* - `readAndSyncHistory()` —— local → network → save network to local 3 **步**
|
||||
* - DTO ↔ UiMessage 映射(纯函数 —— 可单测)
|
||||
* - Result → number 映射(纯函数 —— 可单测)
|
||||
* - 2 个独立 init 任务的数据加载:
|
||||
* - `readGuestQuota()` —— 游客日配 + 总配(仅游客有意义,纯 ChatStorage)
|
||||
* - `readAndSyncHistory()` —— local → network → save network to local 3 步
|
||||
*
|
||||
* 设计目标:
|
||||
* - helpers **无 XState 依赖**(可独立 import / 测试)
|
||||
* - helpers 无 XState 依赖(可独立 import / 测试)
|
||||
* - actors 依赖 helpers(import)
|
||||
* - machine 依赖 actors(import)—— **不**直接依赖 helpers
|
||||
* - machine 依赖 actors(import)—— 不直接依赖 helpers
|
||||
*
|
||||
* 历史:
|
||||
* - `readInitData` + `InitResult` 已**删**(**被** 2 **个**独立 helper **替**换**)
|
||||
* - `AuthStorage` import **已**删**(**只** `readInitData` 用过,**移**到** `chatInit` actor **上**层**调**用**)
|
||||
* - `readInitData` + `InitResult` 已删(被 2 个独立 helper 替换)
|
||||
* - `AuthStorage` import 已删(只 `readInitData` 用过,移到 `chatInit` actor 上层调用)
|
||||
*/
|
||||
|
||||
import type { UiMessage } from "@/models/chat/ui-message";
|
||||
@@ -31,7 +31,7 @@ import { Result } from "@/utils/result";
|
||||
// ============================================================
|
||||
// Constants
|
||||
// ============================================================
|
||||
/** 翻历史每页大小(**仅** `loadMoreHistoryActor` 用) */
|
||||
/** 翻历史每页大小(仅 `loadMoreHistoryActor` 用) */
|
||||
export const PAGE_SIZE = 20;
|
||||
|
||||
// ============================================================
|
||||
@@ -44,7 +44,7 @@ export const chatRepo: IChatRepository = chatRepository;
|
||||
// DTO ↔ UiMessage 映射
|
||||
// ============================================================
|
||||
/**
|
||||
* ChatMessage[] → UiMessage[](**纯函数**)
|
||||
* ChatMessage[] → UiMessage[](纯函数)
|
||||
* - 与 Dart `LocalMessage → ChatMessage` 等价
|
||||
* - 业务事实:`role === "assistant"` → `isFromAI: true`
|
||||
*/
|
||||
@@ -63,9 +63,9 @@ export function localMessagesToUi(
|
||||
}
|
||||
|
||||
/**
|
||||
* ChatSendResponse → UiMessage(**纯函数**)
|
||||
* - 业务事实:后端响应**就是** AI 的回复
|
||||
* - 用后端 `timestamp`(**不**用本地 time)—— 多设备/时区**一致**
|
||||
* ChatSendResponse → UiMessage(纯函数)
|
||||
* - 业务事实:后端响应就是 AI 的回复
|
||||
* - 用后端 `timestamp`(不用本地 time)—— 多设备/时区一致
|
||||
*/
|
||||
export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
|
||||
return {
|
||||
@@ -78,7 +78,7 @@ export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
|
||||
// ============================================================
|
||||
// Result → number 映射
|
||||
// ============================================================
|
||||
/** 日配额 Result → number(**纯函数**) */
|
||||
/** 日配额 Result → number(纯函数) */
|
||||
export function mapQuotaResult(
|
||||
r: Result<{ remaining: number } | null>,
|
||||
fallback: number,
|
||||
@@ -87,7 +87,7 @@ export function mapQuotaResult(
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** 总配额 Result → number(**纯函数**) */
|
||||
/** 总配额 Result → number(纯函数) */
|
||||
export function mapTotalQuotaResult(
|
||||
r: Result<number | null>,
|
||||
fallback: number,
|
||||
@@ -118,26 +118,26 @@ export async function readGuestQuota(): Promise<{
|
||||
// Init 任务 2:history 3 步流(local → network → save)—— `loadHistoryActor` 调
|
||||
// ============================================================
|
||||
/**
|
||||
* 拉 history **流**程(**每**步**都**记**日**志** —— 用户要求"**不**要**删**除**")
|
||||
* 1. **读** local(chatRepo.getLocalMessages)→ **返** localMessages
|
||||
* 2. **读** network(chatRepo.getHistory)→ **返** networkMessages
|
||||
* 3. **用** network **覆**盖** local(chatRepo.saveMessagesToLocal)→ local **被**同步
|
||||
* 拉 history 流程(每步都记日志 —— 用户要求"不要删除")
|
||||
* 1. 读 local(chatRepo.getLocalMessages)→ 返 localMessages
|
||||
* 2. 读 network(chatRepo.getHistory)→ 返 networkMessages
|
||||
* 3. 用 network 覆盖 local(chatRepo.saveMessagesToLocal)→ local 被同步
|
||||
*
|
||||
* **返**回** network **最**终** messages**(authorititative),**不**返** local(UI **不**需**要**过渡**态**)
|
||||
* 返回 network 最终 messages(authorititative),不返 local(UI 不需要过渡态)
|
||||
*
|
||||
* **保**留**日**志**(**前**缀 `[chat-machine]` 与 chat-machine.actors.ts **对**齐**)
|
||||
* 保留日志(前缀 `[chat-machine]` 与 chat-machine.actors.ts 对齐)
|
||||
*/
|
||||
export async function readAndSyncHistory(): Promise<{
|
||||
messages: UiMessage[]; // network **最**终** messages(authorititative)
|
||||
messages: UiMessage[]; // network 最终 messages(authorititative)
|
||||
hasMore: boolean;
|
||||
newOffset: number;
|
||||
localOverwritten: boolean; // true = local **已**被 network **覆**盖**(**写**入** ChatStorage)
|
||||
/** 真实从 local 读到的消息数(**调**试**用**) */
|
||||
localOverwritten: boolean; // true = local 已被 network 覆盖(写入 ChatStorage)
|
||||
/** 真实从 local 读到的消息数(调试用) */
|
||||
localCount: number;
|
||||
/** 真实从 network 读到的消息数(**调**试**用**) */
|
||||
/** 真实从 network 读到的消息数(调试用) */
|
||||
networkCount: number;
|
||||
}> {
|
||||
// 1. **读** local
|
||||
// 1. 读 local
|
||||
const localResult = await chatRepo.getLocalMessages();
|
||||
const localMessages =
|
||||
Result.isOk(localResult) && localResult.data
|
||||
@@ -147,16 +147,16 @@ export async function readAndSyncHistory(): Promise<{
|
||||
count: localMessages.length,
|
||||
});
|
||||
|
||||
// 2. **读** network
|
||||
// 2. 读 network
|
||||
const networkResult = await chatRepo.getHistory(PAGE_SIZE, 0);
|
||||
if (!Result.isOk(networkResult)) {
|
||||
// network 失**败** —— **返**空 messages(**不**让 UI 卡**住**)
|
||||
// network 失败 —— 返空 messages(不让 UI 卡住)
|
||||
console.error(
|
||||
"[chat-machine] loadHistory NETWORK FAILED",
|
||||
networkResult.success ? null : (networkResult as { error: unknown }).error,
|
||||
);
|
||||
return {
|
||||
messages: localMessages, // **退**而**求**其**次**:返 local(**不**然**屏**幕**空**)
|
||||
messages: localMessages, // 退而求其次:返 local(不然屏幕空)
|
||||
hasMore: false,
|
||||
newOffset: 0,
|
||||
localOverwritten: false,
|
||||
@@ -169,7 +169,7 @@ export async function readAndSyncHistory(): Promise<{
|
||||
count: networkUi.length,
|
||||
});
|
||||
|
||||
// 3. **用** network **覆**盖** local("**再**用**网**络**数**据**覆**盖**本**地**数**据"**这**句**的**字**面**实**现**)
|
||||
// 3. 用 network 覆盖 local("再用网络数据覆盖本地数据"这句的字面实现)
|
||||
const saveResult = await chatRepo.saveMessagesToLocal(networkResult.data.messages);
|
||||
const localOverwritten = Result.isOk(saveResult);
|
||||
console.log("[chat-machine] loadHistory SAVE TO LOCAL DONE", {
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
/**
|
||||
* Chat 状态机(XState v5)
|
||||
*
|
||||
* **鉴权解耦**(**事件驱动**):
|
||||
* chat 机器**不**感知鉴权 / **不**管 WebSocket —— 由 chat-screen 派生 loginStatus
|
||||
* chat-screen **只**派 3 个事件:
|
||||
* - `ChatGuestLogin` → 游客会话(**断** WS = 不连)
|
||||
* - `ChatNonGuestLogin { token }` → 非游客会话(**连** WS)
|
||||
* 鉴权解耦(事件驱动):
|
||||
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus
|
||||
* chat-screen 只派 3 个事件:
|
||||
* - `ChatGuestLogin` → 游客会话(断 WS = 不连)
|
||||
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS)
|
||||
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
||||
*
|
||||
* **状态结构**(parent state 模式):
|
||||
* 状态结构(parent state 模式):
|
||||
* - `idle`:屏没挂 / 登出
|
||||
* - `guestSession`(**parent**):游客会话 —— **不** invoke WS
|
||||
* - `userSession`(**parent**):非游客会话 —— **invoke WS**(持久)
|
||||
* - `guestSession`(parent):游客会话 —— 不 invoke WS
|
||||
* - `userSession`(parent):非游客会话 —— invoke WS(持久)
|
||||
*
|
||||
* **init 双任务**(**核**心**重**构**):
|
||||
* - guestSession.initializing:loadQuota + loadHistory **并**行**(`invoke: [...]` **数**组**)
|
||||
* - userSession.initializing:loadHistory + parent-level chatWebSocket **并**行
|
||||
* - **每**个**任务**都**是**独**立** actor,**不**互**相**等**待**(**除**非** `always` barrier)
|
||||
* init 双任务(核心重构):
|
||||
* - guestSession.initializing:loadQuota + loadHistory 并行(`invoke: [...]` 数组)
|
||||
* - userSession.initializing:loadHistory + parent-level chatWebSocket 并行
|
||||
* - 每个任务都是独立 actor,不互相等待(除非 `always` barrier)
|
||||
*
|
||||
* **消息发送路径**(**业务事实**):
|
||||
* - `wsConnected: true`(**仅** userSession 期间)→ **走 WS**,**不**消耗次数(**仅**配额规则)
|
||||
* - `wsConnected: false` → **走 HTTP**,消耗次数(**仅**游客有意义 —— 非游客 0-1=max(0,-1)=0 天然 no-op)
|
||||
* 消息发送路径(业务事实):
|
||||
* - `wsConnected: true`(仅 userSession 期间)→ 走 WS,不消耗次数(仅配额规则)
|
||||
* - `wsConnected: false` → 走 HTTP,消耗次数(仅游客有意义 —— 非游客 0-1=max(0,-1)=0 天然 no-op)
|
||||
* - `ChatWebSocketConnected` 事件 → `wsConnected = true`
|
||||
* - `userSession.exit` action → `wsConnected = false`(WS actor cleanup 时**也**会跑 exit)
|
||||
* - `userSession.exit` action → `wsConnected = false`(WS actor cleanup 时也会跑 exit)
|
||||
*
|
||||
* **配额(**仅**游客)**:
|
||||
* 配额(仅游客):
|
||||
* - 非游客"无消息限制"(业务事实)—— 永不被赋值
|
||||
* - `appendUserMessage` 减 `context.wsConnected ? q : Math.max(0, q-1)` —— WS 已连时**不**递减
|
||||
* - `appendUserMessage` 减 `context.wsConnected ? q : Math.max(0, q-1)` —— WS 已连时不递减
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -75,7 +75,7 @@ export const chatMachine = setup({
|
||||
appendUserMessage: assign(({ context, event }) => {
|
||||
if (event.type !== "ChatSendMessage") return {};
|
||||
|
||||
// 两**个**额度**同**步**更**新**:WS 已**连****不**递减(**实**时**会**话**不**消耗**),HTTP **必**须**减
|
||||
// 两个额度同步更新:WS 已连不递减(实时会话不消耗),HTTP 必须减
|
||||
const newRemaining = context.wsConnected
|
||||
? context.guestRemainingQuota
|
||||
: Math.max(0, context.guestRemainingQuota - 1);
|
||||
@@ -83,8 +83,8 @@ export const chatMachine = setup({
|
||||
? context.guestTotalQuota
|
||||
: Math.max(0, context.guestTotalQuota - 1);
|
||||
|
||||
// 持**久**化**两**个**额度**(** fire-and-forget,**不** await —— assign **是** sync)
|
||||
// daily quota 需**要** today **字**符**串**(**让** ChatStorage 跨**天**判**断** reset)
|
||||
// 持久化两个额度( fire-and-forget,不 await —— assign 是 sync)
|
||||
// daily quota 需要 today 字符串(让 ChatStorage 跨天判断 reset)
|
||||
const today = formatDate();
|
||||
ChatStorage.getInstance().setGuestDailyChatQuota(newRemaining, today);
|
||||
ChatStorage.getInstance().setGuestTotalQuota(newTotal);
|
||||
@@ -117,7 +117,7 @@ export const chatMachine = setup({
|
||||
|
||||
appendUserImage: assign(({ context, event }) => {
|
||||
if (event.type !== "ChatSendImage") return {};
|
||||
// **同** appendUserMessage:两**个**额度**同**步**减
|
||||
// 同 appendUserMessage:两个额度同步减
|
||||
const newRemaining = context.wsConnected
|
||||
? context.guestRemainingQuota
|
||||
: Math.max(0, context.guestRemainingQuota - 1);
|
||||
@@ -211,13 +211,13 @@ export const chatMachine = setup({
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// guestSession(游客会话 —— **不**连 WS,2 **个** init 任务**并**行**)
|
||||
// guestSession(游客会话 —— 不连 WS,2 个 init 任务并行)
|
||||
// ========================================================================
|
||||
guestSession: {
|
||||
initial: "initializing",
|
||||
states: {
|
||||
initializing: {
|
||||
// "always" barrier:两**个**任务**都**完**成**才**进** ready
|
||||
// "always" barrier:两个任务都完成才进 ready
|
||||
always: [
|
||||
{
|
||||
target: "ready",
|
||||
@@ -225,7 +225,7 @@ export const chatMachine = setup({
|
||||
},
|
||||
],
|
||||
invoke: [
|
||||
// 任务 1:加载配额(fromPromise,**返**结果 + onDone 设 flag)
|
||||
// 任务 1:加载配额(fromPromise,返结果 + onDone 设 flag)
|
||||
{
|
||||
id: "loadQuota",
|
||||
src: "loadQuota",
|
||||
@@ -237,13 +237,13 @@ export const chatMachine = setup({
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
// 失败**也**标 loaded,**不**卡 init(**游**客**可**能 quota 服务**不**可**用**,**让** UI **进** ready)
|
||||
// 失败也标 loaded,不卡 init(游客可能 quota 服务不可用,让 UI 进 ready)
|
||||
actions: assign({
|
||||
quotaLoaded: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
// 任务 2:拉 history(fromPromise,**返**回**最**终** network messages**)
|
||||
// 任务 2:拉 history(fromPromise,返回最终 network messages)
|
||||
{
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
@@ -256,7 +256,7 @@ export const chatMachine = setup({
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
// 失败**也**标 loaded,**不**卡 init(**让** UI **还**是**能**进** ready,**屏**幕**可**能**空**)
|
||||
// 失败也标 loaded,不卡 init(让 UI 还是能进 ready,屏幕可能空)
|
||||
actions: assign({
|
||||
historyLoaded: true,
|
||||
}),
|
||||
@@ -266,29 +266,29 @@ export const chatMachine = setup({
|
||||
},
|
||||
ready: {
|
||||
on: {
|
||||
// **配**额**检**查** + 发**送**消息**(3 **条** guard,**按**顺**序**匹**配**)
|
||||
// 1. **总**配**额**(guestTotalQuota)**不**足 → **触**发** quota exceeded dialog,**不**发**送**
|
||||
// 2. **每**日**配**额**(guestRemainingQuota)**不**足 → **同**上**
|
||||
// 3. **两**个**配**额**都** OK + **内**容**非**空** → **发**送** + **减**配**额**
|
||||
// 配额检查 + 发送消息(3 条 guard,按顺序匹配)
|
||||
// 1. 总配额(guestTotalQuota)不足 → 触发 quota exceeded dialog,不发送
|
||||
// 2. 每日配额(guestRemainingQuota)不足 → 同上
|
||||
// 3. 两个配额都 OK + 内容非空 → 发送 + 减配额
|
||||
ChatSendMessage: [
|
||||
// 1. 总配**额** **不**足**(guestTotalQuota <= 0) → **触**发** quota exceeded dialog
|
||||
// 1. 总配额 不足(guestTotalQuota <= 0) → 触发 quota exceeded dialog
|
||||
{
|
||||
guard: ({ context }) => context.guestTotalQuota <= 0,
|
||||
actions: "incrementQuotaExceeded",
|
||||
},
|
||||
// 2. 每**日**配**额** **不**足**(guestRemainingQuota <= 0) → **触**发** quota exceeded dialog
|
||||
// 2. 每日配额 不足(guestRemainingQuota <= 0) → 触发 quota exceeded dialog
|
||||
{
|
||||
guard: ({ context }) => context.guestRemainingQuota <= 0,
|
||||
actions: "incrementQuotaExceeded",
|
||||
},
|
||||
// 3. 两**个**配**额**都** OK + **内**容**非**空** → **发**送**(**只**用** appendUserMessage **自**己**的**日**志**)
|
||||
// 3. 两个配额都 OK + 内容非空 → 发送(只用 appendUserMessage 自己的日志)
|
||||
{
|
||||
actions: "appendUserMessage",
|
||||
guard: ({ event }) => event.content.trim().length > 0,
|
||||
target: "sending",
|
||||
},
|
||||
],
|
||||
// **配**额**检**查** + 发**送**图**片**(**同**样** 3 **条** guard,**不**检**查** "内**容**空"**)
|
||||
// 配额检查 + 发送图片(同样 3 条 guard,不检查 "内容空")
|
||||
ChatSendImage: [
|
||||
{
|
||||
guard: ({ context }) => context.guestTotalQuota <= 0,
|
||||
@@ -303,7 +303,7 @@ export const chatMachine = setup({
|
||||
target: "sending",
|
||||
},
|
||||
],
|
||||
// **删**除 ChatLoadMoreHistory handler —— 游客**无**服**务**端** history,**不**支**持**翻**页**
|
||||
// 删除 ChatLoadMoreHistory handler —— 游客无服务端 history,不支持翻页
|
||||
ChatLogout: "#chat.idle",
|
||||
ChatNonGuestLogin: "#chat.userSession",
|
||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||
@@ -331,18 +331,18 @@ export const chatMachine = setup({
|
||||
},
|
||||
},
|
||||
},
|
||||
// **删**除 loadingMore(游客**无**翻**页**)
|
||||
// 删除 loadingMore(游客无翻页)
|
||||
},
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// userSession(非游客会话 —— WS **父**级 + history **子**级**)
|
||||
// userSession(非游客会话 —— WS 父级 + history 子级)
|
||||
// ========================================================================
|
||||
userSession: {
|
||||
initial: "initializing",
|
||||
exit: "clearWsConnected",
|
||||
invoke: {
|
||||
// 父级:WS 长**连**接(**一**进**来****就**连,跨 initializing/ready/sending/loadingMore)
|
||||
// 父级:WS 长连接(一进来就连,跨 initializing/ready/sending/loadingMore)
|
||||
src: "chatWebSocket",
|
||||
input: ({ event }) => ({
|
||||
token: event.type === "ChatNonGuestLogin" ? event.token : "",
|
||||
@@ -350,7 +350,7 @@ export const chatMachine = setup({
|
||||
},
|
||||
states: {
|
||||
initializing: {
|
||||
// barrier:WS **连**上** + history **加**载**完**成**才**进** ready
|
||||
// barrier:WS 连上 + history 加载完成才进 ready
|
||||
always: [
|
||||
{
|
||||
target: "ready",
|
||||
@@ -358,7 +358,7 @@ export const chatMachine = setup({
|
||||
},
|
||||
],
|
||||
invoke: {
|
||||
// 任务 2:拉 history(**只**调**用** loadHistoryActor,**不**再**用** loadMoreHistoryActor)
|
||||
// 任务 2:拉 history(只调用 loadHistoryActor,不再用 loadMoreHistoryActor)
|
||||
src: "loadHistory",
|
||||
onDone: {
|
||||
actions: assign(({ event }) => ({
|
||||
@@ -370,7 +370,7 @@ export const chatMachine = setup({
|
||||
})),
|
||||
},
|
||||
onError: {
|
||||
// 失败**也**标 loaded,**不**卡 init
|
||||
// 失败也标 loaded,不卡 init
|
||||
actions: assign({
|
||||
isLoadingMore: false,
|
||||
historyLoaded: true,
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
* 注:GuestChatQuota 仍位于 chat-machine.ts(属于业务常量,与上下文配对紧密)。
|
||||
*
|
||||
* 历史:
|
||||
* - `isGuest: boolean` 字段已**删**(由 chat-screen 派生自 `auth.loginStatus === "guest"`)
|
||||
* - `guestRemainingQuota` / `guestTotalQuota` default **改 0**:
|
||||
* - `isGuest: boolean` 字段已删(由 chat-screen 派生自 `auth.loginStatus === "guest"`)
|
||||
* - `guestRemainingQuota` / `guestTotalQuota` default 改 0:
|
||||
* - 游客:chat-screen 派 `ChatInit` → readInitData 调 ChatStorage → onDone 条件赋
|
||||
* - 非游客:**永不被赋值**(业务事实"非游客无消息限制")—— 0 - 1 = max(0, -1) = 0 天然 no-op
|
||||
* - 非游客:永不被赋值(业务事实"非游客无消息限制")—— 0 - 1 = max(0, -1) = 0 天然 no-op
|
||||
* - 0 同时表示"未初始化"和"已耗尽" —— UI 用 `if (isGuest && ...)` 天然过滤
|
||||
*
|
||||
* 新增 `wsConnected: boolean`(**仅** userSession 期间 true):
|
||||
* - **WS 已连** → 消息发送走 WS,**不**消耗次数
|
||||
* - **WS 未连** → 消息发送走 HTTP,消耗次数
|
||||
* 新增 `wsConnected: boolean`(仅 userSession 期间 true):
|
||||
* - WS 已连 → 消息发送走 WS,不消耗次数
|
||||
* - WS 未连 → 消息发送走 HTTP,消耗次数
|
||||
* - `ChatWebSocketConnected` 事件 → true
|
||||
* - `userSession.exit` → false(WS actor cleanup 时**也**会跑 exit)
|
||||
* - `userSession.exit` → false(WS actor cleanup 时也会跑 exit)
|
||||
*/
|
||||
import type { UiMessage } from "@/models/chat/ui-message";
|
||||
|
||||
@@ -22,13 +22,13 @@ export interface ChatState {
|
||||
messages: UiMessage[];
|
||||
isReplyingAI: boolean;
|
||||
/**
|
||||
* WebSocket 连接状态(**仅** userSession 期间 true)
|
||||
* - 决定消息发送**路径**(WS / HTTP)和**是否消耗**次数
|
||||
* - true:走 WS,**不**消耗次数
|
||||
* WebSocket 连接状态(仅 userSession 期间 true)
|
||||
* - 决定消息发送路径(WS / HTTP)和是否消耗次数
|
||||
* - true:走 WS,不消耗次数
|
||||
* - false:走 HTTP,消耗次数
|
||||
*/
|
||||
wsConnected: boolean;
|
||||
/** 游客剩余配额(次/天)—— **仅**游客业务展示用
|
||||
/** 游客剩余配额(次/天)—— 仅游客业务展示用
|
||||
*/
|
||||
guestRemainingQuota: number;
|
||||
/** 游客总配额(仅游客展示用) */
|
||||
@@ -37,14 +37,14 @@ export interface ChatState {
|
||||
isLoadingMore: boolean;
|
||||
hasMore: boolean;
|
||||
historyOffset: number;
|
||||
/** 游客配额加载完成标**志**(**仅** guestSession 期间) —— default false
|
||||
* - guestSession.initializing **走** always barrier:`quotaLoaded && historyLoaded` **才**进** ready
|
||||
* - userSession **永不为** true(**不**调 loadQuota)—— OK,**不**影响 barrier
|
||||
/** 游客配额加载完成标志(仅 guestSession 期间) —— default false
|
||||
* - guestSession.initializing 走 always barrier:`quotaLoaded && historyLoaded` 才进 ready
|
||||
* - userSession 永不为 true(不调 loadQuota)—— OK,不影响 barrier
|
||||
*/
|
||||
quotaLoaded: boolean;
|
||||
/** history 加**载**完成标**志**(initial load —— local → network → save 跑**完**)
|
||||
/** history 加载完成标志(initial load —— local → network → save 跑完)
|
||||
* - guestSession.initializing / userSession.initializing 走 always barrier
|
||||
* - **不**被 `loadMoreHistoryActor` 设**置**(**翻**页**是**另**一**码**事**)
|
||||
* - 不被 `loadMoreHistoryActor` 设置(翻页是另一码事)
|
||||
*/
|
||||
historyLoaded: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user