feat(chat): sync multi-role backend APIs
This commit is contained in:
@@ -26,10 +26,10 @@ function createStorageState(input: {
|
||||
|
||||
describe("chat cache identity", () => {
|
||||
it("isolates the same owner by character", () => {
|
||||
const elio = buildChatConversationKey("user:account-1", "character_elio");
|
||||
const elio = buildChatConversationKey("user:account-1", "elio");
|
||||
const aria = buildChatConversationKey("user:account-1", "character_aria");
|
||||
|
||||
expect(elio).toBe("user:account-1::character:character_elio");
|
||||
expect(elio).toBe("user:account-1::character:elio");
|
||||
expect(aria).toBe("user:account-1::character:character_aria");
|
||||
expect(elio).not.toBe(aria);
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("ChatMediaCacheCoordinator identity isolation", () => {
|
||||
expect(result).toEqual(Result.ok(null));
|
||||
expect(getMedia).toHaveBeenCalledTimes(1);
|
||||
expect(getMedia).toHaveBeenCalledWith(
|
||||
"user:account-a::character:character_elio:message-1:image",
|
||||
"user:account-a::character:elio:message-1:image",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("ChatMediaCacheCoordinator identity isolation", () => {
|
||||
);
|
||||
|
||||
await coordinator.getCachedMedia({
|
||||
characterId: "character_elio",
|
||||
characterId: "elio",
|
||||
messageId: "shared-message-id",
|
||||
kind: "image",
|
||||
});
|
||||
@@ -57,7 +57,7 @@ describe("ChatMediaCacheCoordinator identity isolation", () => {
|
||||
});
|
||||
|
||||
expect(getMedia.mock.calls).toEqual([
|
||||
["user:account-a::character:character_elio:shared-message-id:image"],
|
||||
["user:account-a::character:elio:shared-message-id:image"],
|
||||
["user:account-a::character:character_aria:shared-message-id:image"],
|
||||
]);
|
||||
});
|
||||
@@ -112,7 +112,7 @@ describe("ChatMediaCacheCoordinator identity isolation", () => {
|
||||
kind: "image",
|
||||
remoteUrl: "https://media.example/expired.jpg",
|
||||
},
|
||||
"user:account-a::character:character_elio",
|
||||
"user:account-a::character:elio",
|
||||
);
|
||||
|
||||
expect(Result.isErr(result)).toBe(true);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ICharacterRepository } from "@/data/repositories/interfaces";
|
||||
import type { CharacterListResponse } from "@/data/schemas/character";
|
||||
import { CharacterApi, characterApi } from "@/data/services/api";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import { createLazySingleton } from "./lazy_singleton";
|
||||
|
||||
export class CharacterRepository implements ICharacterRepository {
|
||||
constructor(private readonly api: CharacterApi) {}
|
||||
|
||||
async getChatCharacters(): Promise<Result<CharacterListResponse>> {
|
||||
return Result.wrap(() => this.api.getChatCharacters());
|
||||
}
|
||||
}
|
||||
|
||||
export const getCharacterRepository = createLazySingleton<ICharacterRepository>(
|
||||
() => new CharacterRepository(characterApi),
|
||||
);
|
||||
@@ -5,6 +5,9 @@ import type {
|
||||
} from "@/data/repositories/interfaces";
|
||||
import {
|
||||
ChatHistoryResponse,
|
||||
ChatPreviewsResponse,
|
||||
ChatSyncRequest,
|
||||
ChatSyncRequestSchema,
|
||||
ChatSendResponse,
|
||||
SendMessageRequestSchema,
|
||||
UnlockHistoryRequestSchema,
|
||||
@@ -27,13 +30,43 @@ export class ChatRemoteDataSource {
|
||||
const request = SendMessageRequestSchema.parse({
|
||||
characterId,
|
||||
message,
|
||||
image: options?.image ?? "",
|
||||
...(options?.imageId ? { imageId: options.imageId } : {}),
|
||||
...(options?.imageThumbUrl
|
||||
? { imageThumbUrl: options.imageThumbUrl }
|
||||
: {}),
|
||||
...(options?.imageMediumUrl
|
||||
? { imageMediumUrl: options.imageMediumUrl }
|
||||
: {}),
|
||||
...(options?.imageOriginalUrl
|
||||
? { imageOriginalUrl: options.imageOriginalUrl }
|
||||
: {}),
|
||||
...(options?.imageWidth !== undefined
|
||||
? { imageWidth: options.imageWidth }
|
||||
: {}),
|
||||
...(options?.imageHeight !== undefined
|
||||
? { imageHeight: options.imageHeight }
|
||||
: {}),
|
||||
useWebSocket: options?.useWebSocket ?? false,
|
||||
});
|
||||
return await this.api.sendMessage(request, options);
|
||||
});
|
||||
}
|
||||
|
||||
async getPreviews(
|
||||
options?: ChatRequestOptions,
|
||||
): Promise<Result<ChatPreviewsResponse>> {
|
||||
return Result.wrap(() => this.api.getPreviews(options));
|
||||
}
|
||||
|
||||
async syncGuestHistory(
|
||||
request: ChatSyncRequest,
|
||||
options?: ChatRequestOptions,
|
||||
): Promise<Result<void>> {
|
||||
return Result.wrap(() =>
|
||||
this.api.syncGuestHistory(ChatSyncRequestSchema.parse(request), options),
|
||||
);
|
||||
}
|
||||
|
||||
async getHistory(
|
||||
characterId: string,
|
||||
limit = 50,
|
||||
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
} from "@/data/repositories/interfaces";
|
||||
import type {
|
||||
ChatHistoryResponse,
|
||||
ChatPreviewsResponse,
|
||||
ChatSyncRequest,
|
||||
ChatMessage,
|
||||
ChatSendResponse,
|
||||
UnlockHistoryResponse,
|
||||
@@ -55,6 +57,19 @@ export class ChatRepository implements IChatRepository {
|
||||
return this.remote.getHistory(characterId, limit, offset, options);
|
||||
}
|
||||
|
||||
async getPreviews(
|
||||
options?: ChatRequestOptions,
|
||||
): Promise<Result<ChatPreviewsResponse>> {
|
||||
return this.remote.getPreviews(options);
|
||||
}
|
||||
|
||||
async syncGuestHistory(
|
||||
request: ChatSyncRequest,
|
||||
options?: ChatRequestOptions,
|
||||
): Promise<Result<void>> {
|
||||
return this.remote.syncGuestHistory(request, options);
|
||||
}
|
||||
|
||||
/** 解锁单条历史付费 / 私密消息。 */
|
||||
async unlockPrivateMessage(
|
||||
input: UnlockPrivateMessageInput,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export * from "./auth_repository";
|
||||
export * from "./chat_repository";
|
||||
export * from "./character_repository";
|
||||
export * from "./feedback_repository";
|
||||
export * from "./metrics_repository";
|
||||
export * from "./payment_repository";
|
||||
@@ -11,6 +12,7 @@ export * from "./private_room_repository";
|
||||
export * from "./user_repository";
|
||||
export * from "./interfaces/iauth_repository";
|
||||
export * from "./interfaces/ichat_repository";
|
||||
export * from "./interfaces/icharacter_repository";
|
||||
export * from "./interfaces/ifeedback_repository";
|
||||
export * from "./interfaces/imetrics_repository";
|
||||
export * from "./interfaces/ipayment_repository";
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { CharacterListResponse } from "@/data/schemas/character";
|
||||
import type { Result } from "@/utils/result";
|
||||
|
||||
export interface ICharacterRepository {
|
||||
getChatCharacters(): Promise<Result<CharacterListResponse>>;
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
import type {
|
||||
ChatHistoryResponse,
|
||||
ChatPreviewsResponse,
|
||||
ChatSyncRequest,
|
||||
ChatImageData,
|
||||
ChatLockDetailData,
|
||||
ChatLockType,
|
||||
@@ -47,7 +49,12 @@ export interface ChatRequestOptions {
|
||||
}
|
||||
|
||||
export interface ChatSendOptions extends ChatRequestOptions {
|
||||
image?: string;
|
||||
imageId?: string;
|
||||
imageThumbUrl?: string;
|
||||
imageMediumUrl?: string;
|
||||
imageOriginalUrl?: string;
|
||||
imageWidth?: number;
|
||||
imageHeight?: number;
|
||||
useWebSocket?: boolean;
|
||||
}
|
||||
|
||||
@@ -67,6 +74,17 @@ export interface IChatRepository {
|
||||
options?: ChatRequestOptions,
|
||||
): Promise<Result<ChatHistoryResponse>>;
|
||||
|
||||
/** 批量获取所有可聊天角色的最新消息。 */
|
||||
getPreviews(
|
||||
options?: ChatRequestOptions,
|
||||
): Promise<Result<ChatPreviewsResponse>>;
|
||||
|
||||
/** 把一个角色的游客历史同步到正式账号。 */
|
||||
syncGuestHistory(
|
||||
request: ChatSyncRequest,
|
||||
options?: ChatRequestOptions,
|
||||
): Promise<Result<void>>;
|
||||
|
||||
/** 解锁单条历史付费 / 私密消息。 */
|
||||
unlockPrivateMessage(
|
||||
input: UnlockPrivateMessageInput,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export * from "./iauth_repository";
|
||||
export * from "./ichat_repository";
|
||||
export * from "./icharacter_repository";
|
||||
export * from "./ifeedback_repository";
|
||||
export * from "./imetrics_repository";
|
||||
export * from "./ipayment_repository";
|
||||
|
||||
@@ -5,13 +5,13 @@ import type {
|
||||
import type { Result } from "@/utils/result";
|
||||
|
||||
export interface GetPrivateAlbumsInput {
|
||||
characterId?: string;
|
||||
characterId: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface IPrivateRoomRepository {
|
||||
getAlbums(
|
||||
input?: GetPrivateAlbumsInput,
|
||||
input: GetPrivateAlbumsInput,
|
||||
): Promise<Result<PrivateAlbumsResponse>>;
|
||||
|
||||
unlockAlbum(
|
||||
|
||||
@@ -19,7 +19,7 @@ export class PrivateRoomRepository implements IPrivateRoomRepository {
|
||||
constructor(private readonly api: PrivateRoomApi) {}
|
||||
|
||||
getAlbums(
|
||||
input: GetPrivateAlbumsInput = {},
|
||||
input: GetPrivateAlbumsInput,
|
||||
): Promise<Result<PrivateAlbumsResponse>> {
|
||||
return Result.wrap(() => this.api.getAlbums(input));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user