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:
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* ChatRepository
|
||||
*
|
||||
* 聊天仓库:编排远程 ChatApi + 本地 LocalChatStorage(Dexie/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 → LocalMessage(sessionId="")
|
||||
* - 读: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(),
|
||||
);
|
||||
Reference in New Issue
Block a user