feat(data): add repository layer with Auth/Chat/User/Metrics + shared Result<T>

引入仓库层(`src/data/repositories/`),对齐原 Dart `lib/data/repositories/`
的四个仓库:Auth(hybrid: API+AuthStorage+UserStorage)、Chat(hybrid:
API+LocalChatStorage via Dexie)、User(纯远程)、Metrics(纯远程)。

同步抽取 4 个 `*_api.ts` 中重复的 envelope-unwrap 代码到
`src/data/api/response_helper.ts`,失败时抛 `ApiError`(之前抛裸 Error)。

新建 `src/data/result.ts` 提供仓库层统一 `Result<T>`(`{success,data}` 形态),
与 `src/data/storage/result.ts`(`{kind,value}` 形态)并存。

更新 `barrelsby.json` 让 `src/data/repositories/` 加入 barrel 生成。
This commit is contained in:
2026-06-08 19:16:28 +08:00
parent 75e685d418
commit a240f5965d
13 changed files with 695 additions and 71 deletions
+150
View File
@@ -0,0 +1,150 @@
"use client";
/**
* ChatRepository
*
* 聊天仓库:编排远程 ChatApi + 本地 LocalChatStorageDexie/IndexedDB)。
* 行为对齐原 Dart `lib/data/repositories/chat_repository_impl.dart`
* - `saveMessagesToLocal` 保留「先清后写」语义(Dart `box.clear()` + bulk add
* - 替代 Dart 同步 `int get localMessageCount`,改为异步 `getLocalMessageCount()`
* Dexie 异步 API 决定)
*
* ChatMessage ↔ LocalMessage 的字段映射在仓库内 inline 完成(用户决策):
* - 写:ChatMessage 的 id/role/content/createdAt → LocalMessagesessionId=""
* - 读:LocalMessage 的 id/role/content/createdAt → ChatMessage(丢弃 sessionId
*
* 原始 Dart: lib/data/repositories/chat_repository_impl.dart
*/
import { ChatApi, chatApi } from "@/data/api";
import { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
import { ChatMessage } from "@/data/dto/chat/chat_message";
import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
import { SendMessageRequest } from "@/data/dto/chat/send_message_request";
import { Result } from "@/data/result";
import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage";
import { LocalMessage } from "@/data/storage/chat/local_message";
export class ChatRepository {
constructor(
private readonly api: ChatApi,
private readonly localStorage: LocalChatStorage,
) {}
// ============ 远程操作 ============
/** 发送一条消息。 */
async sendMessage(
message: string,
options?: { image?: string; useWebSocket?: boolean },
): Promise<Result<ChatSendResponse>> {
return Result.wrap(async () => {
const request = SendMessageRequest.from({
message,
image: options?.image ?? "",
useWebSocket: options?.useWebSocket ?? false,
});
return await this.api.sendMessage(request);
});
}
/** 获取聊天历史,分页参数默认 limit=50, offset=0。 */
async getHistory(
limit = 50,
offset = 0,
): Promise<Result<ChatHistoryResponse>> {
return Result.wrap(() => this.api.getHistory(limit, offset));
}
// ============ 本地(Dexie)操作 ============
/** 把一条消息写入本地存储。 */
async saveMessageToLocal(message: ChatMessage): Promise<Result<void>> {
return this._mapLocalStorageResult(
this.localStorage.saveMessage(this._chatToLocal(message)),
);
}
/**
* 批量覆盖写入:先清空本地存储,再写入新列表。
* 保留 Dart 端的「先清后写」语义(用于同步场景的整批刷新)。
*/
async saveMessagesToLocal(
messages: readonly ChatMessage[],
): Promise<Result<void>> {
const cleared = await this.localStorage.clearAll();
if (cleared.kind === "failure") {
return Result.err(cleared.error);
}
return this._mapLocalStorageResult(
this.localStorage.saveMessages(messages.map((m) => this._chatToLocal(m))),
);
}
/** 读取所有本地消息,按 dbId 升序。 */
async getLocalMessages(): Promise<Result<ChatMessage[]>> {
const result = await this.localStorage.getAllMessages();
if (result.kind === "failure") {
return Result.err(result.error);
}
return Result.ok(result.value.map((lm) => this._localToChat(lm)));
}
/** 清空本地消息。 */
async clearLocalMessages(): Promise<Result<void>> {
return this._mapLocalStorageResult(this.localStorage.clearAll());
}
/**
* 获取本地消息数量。
* 替代 Dart 的同步 `int get localMessageCount`——Dexie 异步 API 决定必须 async。
*/
async getLocalMessageCount(): Promise<Result<number>> {
return this._mapLocalStorageResult(this.localStorage.getMessageCount());
}
// ============ 私有助手 ============
/** ChatMessage → LocalMessage:丢弃 wire 字段,注入 storage 层的 sessionId=""。 */
private _chatToLocal(message: ChatMessage): LocalMessage {
return LocalMessage.from({
id: message.id,
role: message.role,
content: message.content,
createdAt: message.createdAt,
sessionId: "",
});
}
/** LocalMessage → ChatMessage:丢弃 storage 层的 sessionId。 */
private _localToChat(local: LocalMessage): ChatMessage {
return ChatMessage.from({
id: local.id,
role: local.role,
content: local.content,
createdAt: local.createdAt,
});
}
/**
* 把 storage 层 `{kind, value/error}` 形态的 `Result<T>` 桥接到仓库层
* `{success, data/error}` 形态。错误统一用 `Error` 包装。
*/
private async _mapLocalStorageResult<T>(
promise: Promise<
| { readonly kind: "success"; readonly value: T }
| { readonly kind: "failure"; readonly error: unknown }
>,
): Promise<Result<T>> {
const r = await promise;
if (r.kind === "success") {
return Result.ok(r.value);
}
return Result.err(r.error);
}
}
/** 全局单例。 */
export const chatRepository = new ChatRepository(
chatApi,
LocalChatStorage.getInstance(),
);