194 lines
5.7 KiB
TypeScript
194 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import { ChatApi, chatApi } from "@/data/services/api";
|
|
import {
|
|
ChatHistoryResponse,
|
|
ChatMessage,
|
|
ChatSendResponse,
|
|
SendMessageRequest,
|
|
UnlockHistoryResponse,
|
|
UnlockPrivateRequest,
|
|
UnlockPrivateResponse,
|
|
} from "@/data/dto/chat";
|
|
import { Result } from "@/utils";
|
|
import type { IChatRepository } from "@/data/repositories/interfaces";
|
|
import { LocalChatStorage, LocalMessage } from "@/data/storage/chat";
|
|
|
|
export class ChatRepository implements IChatRepository {
|
|
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));
|
|
}
|
|
|
|
/** 解锁私密消息。 */
|
|
async unlockPrivateMessage(
|
|
messageId: string,
|
|
): Promise<Result<UnlockPrivateResponse>> {
|
|
return Result.wrap(() =>
|
|
this.api.unlockPrivateMessage(UnlockPrivateRequest.from({ messageId })),
|
|
);
|
|
}
|
|
|
|
/** 一键解锁历史锁定消息。 */
|
|
async unlockHistory(): Promise<Result<UnlockHistoryResponse>> {
|
|
return Result.wrap(() => this.api.unlockHistory());
|
|
}
|
|
|
|
/** 把本地缓存中的私密消息标记为已解锁。 */
|
|
async markPrivateMessageUnlockedInLocal(
|
|
messageId: string,
|
|
content: string,
|
|
): Promise<Result<void>> {
|
|
return Result.wrap(async () => {
|
|
const localResult = await this.getLocalMessages();
|
|
if (Result.isErr(localResult)) {
|
|
throw localResult.error;
|
|
}
|
|
|
|
let changed = false;
|
|
const updatedMessages = localResult.data.map((message) => {
|
|
if (message.id !== messageId) return message;
|
|
changed = true;
|
|
return ChatMessage.from({
|
|
...message.toJson(),
|
|
content,
|
|
lockDetail: {
|
|
...message.lockDetail,
|
|
locked: false,
|
|
showContent: true,
|
|
showUpgrade: false,
|
|
hint: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
if (!changed) return;
|
|
|
|
const saveResult = await this.saveMessagesToLocal(updatedMessages);
|
|
if (Result.isErr(saveResult)) {
|
|
throw saveResult.error;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============ 本地(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.success) {
|
|
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.success) {
|
|
return Result.err(result.error);
|
|
}
|
|
return Result.ok(result.data.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,
|
|
type: message.type,
|
|
content: message.content,
|
|
createdAt: message.createdAt,
|
|
audioUrl: message.audioUrl,
|
|
image: message.image,
|
|
lockDetail: message.lockDetail,
|
|
sessionId: "",
|
|
});
|
|
}
|
|
|
|
/** LocalMessage → ChatMessage:丢弃 storage 层的 sessionId。 */
|
|
private _localToChat(local: LocalMessage): ChatMessage {
|
|
return ChatMessage.from({
|
|
id: local.id,
|
|
role: local.role,
|
|
type: local.type,
|
|
content: local.content,
|
|
createdAt: local.createdAt,
|
|
audioUrl: local.audioUrl,
|
|
image: local.image,
|
|
lockDetail: local.lockDetail,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 把 storage 层 `Result<T>` 透传:仓库与 storage 现在共享同一全局
|
|
* `Result<T>`(来自 `@/utils/result`),不需要任何形状转换。
|
|
* 该方法保留仅为未来万一又出现差异化时方便快速插入适配。
|
|
*/
|
|
private async _mapLocalStorageResult<T>(
|
|
promise: Promise<Result<T>>,
|
|
): Promise<Result<T>> {
|
|
return await promise;
|
|
}
|
|
}
|
|
|
|
/** 全局单例。 */
|
|
export const chatRepository = new ChatRepository(
|
|
chatApi,
|
|
LocalChatStorage.getInstance(),
|
|
);
|