/** * Chat 状态机:纯函数 + 数据加载 * * 从 `chat-machine.ts` 抽出的"helpers"段: * - 翻页大小常量 * - 仓库注入(接口类型 → 单例) * - DTO ↔ UiMessage 映射(纯函数 —— 可单测) * - Result → number 映射(纯函数 —— 可单测) * - 2 个独立 init 任务的数据加载: * - `readGuestQuota()` —— 游客日配 + 总配(仅游客有意义,纯 ChatStorage) * - `readAndSyncHistory()` —— local → network → save network to local 3 步 * * 设计目标: * - helpers 无 XState 依赖(可独立 import / 测试) * - actors 依赖 helpers(import) * - machine 依赖 actors(import)—— 不直接依赖 helpers * * 历史: * - `readInitData` + `InitResult` 已删(被 2 个独立 helper 替换) * - `AuthStorage` import 已删(只 `readInitData` 用过,移到 `chatInit` actor 上层调用) */ import type { UiMessage } from "@/models/chat/ui-message"; import { chatRepository } from "@/data/repositories/chat_repository"; import type { IChatRepository } from "@/data/repositories/interfaces"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; import { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; import { formatDate } from "@/utils/date"; import { Result } from "@/utils/result"; // ============================================================ // Constants // ============================================================ /** 翻历史每页大小(仅 `loadMoreHistoryActor` 用) */ export const PAGE_SIZE = 20; // ============================================================ // Repository injection // ============================================================ // 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例 export const chatRepo: IChatRepository = chatRepository; // ============================================================ // DTO ↔ UiMessage 映射 // ============================================================ /** * ChatMessage[] → UiMessage[](纯函数) * - 与 Dart `LocalMessage → ChatMessage` 等价 * - 业务事实:`role === "assistant"` → `isFromAI: true` */ export function localMessagesToUi( records: ReadonlyArray<{ content: string; role: string; createdAt: string; }>, ): UiMessage[] { return records.map((m) => ({ content: m.content, isFromAI: m.role === "assistant", date: m.createdAt, })); } /** * ChatSendResponse → UiMessage(纯函数) * - 业务事实:后端响应就是 AI 的回复 * - 用后端 `timestamp`(不用本地 time)—— 多设备/时区一致 */ export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage { return { content: response.reply, isFromAI: true, date: formatDate(new Date(response.timestamp)), }; } // ============================================================ // Result → number 映射 // ============================================================ /** 日配额 Result → number(纯函数) */ export function mapQuotaResult( r: Result<{ remaining: number } | null>, fallback: number, ): number { if (r.success && r.data != null) return r.data.remaining; return fallback; } /** 总配额 Result → number(纯函数) */ export function mapTotalQuotaResult( r: Result, fallback: number, ): number { if (r.success && r.data != null) return r.data; return fallback; } 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, 0), }; } // ============================================================ // 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 被同步 * * 返回 network 最终 messages(authorititative),不返 local(UI 不需要过渡态) * * 保留日志(前缀 `[chat-machine]` 与 chat-machine.actors.ts 对齐) */ export async function readAndSyncHistory(): Promise<{ messages: UiMessage[]; // network 最终 messages(authorititative) hasMore: boolean; newOffset: number; localOverwritten: boolean; // true = local 已被 network 覆盖(写入 ChatStorage) /** 真实从 local 读到的消息数(调试用) */ localCount: number; /** 真实从 network 读到的消息数(调试用) */ networkCount: number; }> { // 1. 读 local const localResult = await chatRepo.getLocalMessages(); const localMessages = Result.isOk(localResult) && localResult.data ? localMessagesToUi(localResult.data) : []; console.log("[chat-machine] loadHistory LOCAL DONE", { count: localMessages.length, }); // 2. 读 network const networkResult = await chatRepo.getHistory(PAGE_SIZE, 0); if (!Result.isOk(networkResult)) { // network 失败 —— 返空 messages(不让 UI 卡住) console.error( "[chat-machine] loadHistory NETWORK FAILED", networkResult.success ? null : (networkResult as { error: unknown }).error, ); return { messages: localMessages, // 退而求其次:返 local(不然屏幕空) hasMore: false, newOffset: 0, localOverwritten: false, localCount: localMessages.length, networkCount: 0, }; } const networkUi = localMessagesToUi(networkResult.data.messages); console.log("[chat-machine] loadHistory 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] loadHistory SAVE TO LOCAL DONE", { localOverwritten, }); return { messages: networkUi, // authorititative hasMore: networkUi.length >= PAGE_SIZE, newOffset: networkUi.length, localOverwritten, localCount: localMessages.length, networkCount: networkUi.length, }; }