184 lines
5.2 KiB
TypeScript
184 lines
5.2 KiB
TypeScript
import type {
|
|
CacheRemoteChatMediaInput,
|
|
ChatMediaLookupInput,
|
|
ChatRequestOptions,
|
|
ChatSendOptions,
|
|
IChatRepository,
|
|
UnlockPrivateMessageInput,
|
|
UnlockedPrivateMessageLocalPatch,
|
|
} from "@/data/repositories/interfaces";
|
|
import type {
|
|
ChatHistoryResponse,
|
|
ChatPreviewsResponse,
|
|
ChatMessage,
|
|
ChatSendResponse,
|
|
UnlockHistoryResponse,
|
|
UnlockPrivateResponse,
|
|
} from "@/data/schemas/chat";
|
|
import { chatApi } from "@/data/services/api";
|
|
import {
|
|
LocalChatMediaStorage,
|
|
LocalChatStorage,
|
|
type LocalChatMediaRow,
|
|
} from "@/data/storage/chat";
|
|
import type { Result } from "@/utils/result";
|
|
|
|
import { ChatLocalMessageStore } from "./chat_local_message_store";
|
|
import { ChatMediaCacheCoordinator } from "./chat_media_cache_coordinator";
|
|
import { ChatRemoteDataSource } from "./chat_remote_data_source";
|
|
import { createLazySingleton } from "./lazy_singleton";
|
|
|
|
export class ChatRepository implements IChatRepository {
|
|
constructor(
|
|
private readonly remote: ChatRemoteDataSource,
|
|
private readonly localMessages: ChatLocalMessageStore,
|
|
private readonly mediaCache: ChatMediaCacheCoordinator,
|
|
) {}
|
|
|
|
// ============ 远程操作 ============
|
|
|
|
/** 发送一条消息。 */
|
|
async sendMessage(
|
|
characterId: string,
|
|
message: string,
|
|
options?: ChatSendOptions,
|
|
): Promise<Result<ChatSendResponse>> {
|
|
return this.remote.sendMessage(characterId, message, options);
|
|
}
|
|
|
|
/** 获取聊天历史,分页参数默认 limit=50, offset=0。 */
|
|
async getHistory(
|
|
characterId: string,
|
|
limit = 50,
|
|
offset = 0,
|
|
options?: ChatRequestOptions,
|
|
): Promise<Result<ChatHistoryResponse>> {
|
|
return this.remote.getHistory(characterId, limit, offset, options);
|
|
}
|
|
|
|
async getPreviews(
|
|
options?: ChatRequestOptions,
|
|
): Promise<Result<ChatPreviewsResponse>> {
|
|
return this.remote.getPreviews(options);
|
|
}
|
|
|
|
/** 解锁单条历史付费 / 私密消息。 */
|
|
async unlockPrivateMessage(
|
|
input: UnlockPrivateMessageInput,
|
|
options?: ChatRequestOptions,
|
|
): Promise<Result<UnlockPrivateResponse>> {
|
|
return this.remote.unlockPrivateMessage(input, options);
|
|
}
|
|
|
|
/** 一键解锁历史锁定消息。 */
|
|
async unlockHistory(
|
|
characterId: string,
|
|
options?: ChatRequestOptions,
|
|
): Promise<Result<UnlockHistoryResponse>> {
|
|
return this.remote.unlockHistory(characterId, options);
|
|
}
|
|
|
|
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
|
|
async markPrivateMessageUnlockedInLocal(
|
|
messageId: string,
|
|
patch?: UnlockedPrivateMessageLocalPatch,
|
|
cacheIdentity?: string,
|
|
): Promise<Result<void>> {
|
|
return this.localMessages.markMessageUnlocked(
|
|
messageId,
|
|
patch,
|
|
cacheIdentity,
|
|
);
|
|
}
|
|
|
|
// ============ 本地(Dexie)操作 ============
|
|
|
|
/** 把一条消息写入本地存储。 */
|
|
async saveMessageToLocal(
|
|
message: ChatMessage,
|
|
cacheIdentity?: string,
|
|
): Promise<Result<void>> {
|
|
return this.localMessages.saveMessage(message, cacheIdentity);
|
|
}
|
|
|
|
/**
|
|
* 批量覆盖写入:先清空本地存储,再写入新列表。
|
|
* 用于网络历史同步后的整批刷新。
|
|
*/
|
|
async saveMessagesToLocal(
|
|
messages: readonly ChatMessage[],
|
|
cacheIdentity?: string,
|
|
): Promise<Result<void>> {
|
|
return this.localMessages.saveMessages(messages, cacheIdentity);
|
|
}
|
|
|
|
/** 读取所有本地消息,按 dbId 升序。 */
|
|
async getLocalMessages(
|
|
cacheIdentity?: string,
|
|
): Promise<Result<readonly ChatMessage[]>> {
|
|
return this.localMessages.getMessages(cacheIdentity);
|
|
}
|
|
|
|
/** 清空本地消息。 */
|
|
async clearLocalMessages(cacheIdentity?: string): Promise<Result<void>> {
|
|
return this.localMessages.clearMessages(cacheIdentity);
|
|
}
|
|
|
|
/**
|
|
* 获取本地消息数量。
|
|
* Dexie 查询是异步操作,因此始终返回 Promise。
|
|
*/
|
|
async getLocalMessageCount(cacheIdentity?: string): Promise<Result<number>> {
|
|
return this.localMessages.getMessageCount(cacheIdentity);
|
|
}
|
|
|
|
// ============ 本地媒体缓存 ============
|
|
|
|
async getCachedMedia(
|
|
input: ChatMediaLookupInput,
|
|
): Promise<Result<LocalChatMediaRow | null>> {
|
|
return this.mediaCache.getCachedMedia(input);
|
|
}
|
|
|
|
async cacheRemoteMedia(
|
|
input: CacheRemoteChatMediaInput,
|
|
cacheIdentity?: string,
|
|
): Promise<Result<LocalChatMediaRow>> {
|
|
return this.mediaCache.cacheRemoteMedia(input, cacheIdentity);
|
|
}
|
|
|
|
async prefetchMediaForMessages(
|
|
messages: readonly ChatMessage[],
|
|
characterId: string,
|
|
cacheIdentity?: string,
|
|
): Promise<Result<void>> {
|
|
return this.mediaCache.prefetchMediaForMessages(
|
|
messages,
|
|
characterId,
|
|
cacheIdentity,
|
|
);
|
|
}
|
|
|
|
async prefetchMediaForSendResponse(
|
|
response: ChatSendResponse,
|
|
characterId: string,
|
|
cacheIdentity?: string,
|
|
): Promise<Result<void>> {
|
|
return this.mediaCache.prefetchMediaForSendResponse(
|
|
response,
|
|
characterId,
|
|
cacheIdentity,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** 全局懒单例。 */
|
|
export const getChatRepository = createLazySingleton<IChatRepository>(
|
|
() =>
|
|
new ChatRepository(
|
|
new ChatRemoteDataSource(chatApi),
|
|
new ChatLocalMessageStore(LocalChatStorage.getInstance()),
|
|
new ChatMediaCacheCoordinator(LocalChatMediaStorage.getInstance()),
|
|
),
|
|
);
|