feat(chat): sync multi-role backend APIs

This commit is contained in:
2026-07-20 11:29:54 +08:00
parent 16b5c16e76
commit b6fdc912ae
84 changed files with 1488 additions and 439 deletions
@@ -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,
+15
View File
@@ -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,
+2
View File
@@ -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));
}