feat(characters): support character-scoped conversations

This commit is contained in:
2026-07-17 11:42:31 +08:00
parent 93efcb6604
commit 2796010971
85 changed files with 1645 additions and 251 deletions
@@ -4,8 +4,8 @@ import { Result } from "@/utils/result";
const getHistoryMock = vi.hoisted(() => vi.fn());
vi.mock("@/data/repositories/chat_repository", () => ({
getChatRepository: () => ({ getHistory: getHistoryMock }),
vi.mock("@/data/repositories/chat_repository_loader", () => ({
loadChatRepository: async () => ({ getHistory: getHistoryMock }),
}));
import {
@@ -31,10 +31,22 @@ describe("fetchSplashLatestMessagePreview", () => {
const result = await fetchSplashLatestMessagePreview();
expect(getHistoryMock).toHaveBeenCalledWith(1, 0);
expect(getHistoryMock).toHaveBeenCalledWith(
"character_elio",
1,
0,
);
expect(Result.isOk(result) && result.data).toBe("Latest message");
});
it("requests the selected character preview", async () => {
getHistoryMock.mockResolvedValue(Result.ok({ messages: [] }));
await fetchSplashLatestMessagePreview("character_aria");
expect(getHistoryMock).toHaveBeenCalledWith("character_aria", 1, 0);
});
it("returns null when history is empty", async () => {
getHistoryMock.mockResolvedValue(Result.ok({ messages: [] }));
+1
View File
@@ -7,6 +7,7 @@ import { Result, type Result as ResultT } from "@/utils/result";
import { localChatMediaRowToBlob } from "./chat_media_blob";
export interface ResolveCachedChatMediaBlobInput {
characterId: string;
messageId: string;
kind: ChatMediaKind;
}
+15 -12
View File
@@ -1,8 +1,9 @@
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
import {
resolveChatCacheOwnerKey,
resolveChatConversationKey,
type ChatCacheIdentityResolver,
} from "@/data/repositories/chat_cache_identity";
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
import { Result, type Result as ResultT } from "@/utils/result";
import type { SplashLatestMessageCache } from "./splash_latest_message_cache";
@@ -10,21 +11,23 @@ import { getLatestSplashMessagePreview } from "./splash_latest_message_preview";
export interface LoadSplashLatestMessagePreviewInput {
cache: SplashLatestMessageCache;
fetchPreview?: () => Promise<ResultT<string | null>>;
characterId?: string;
fetchPreview?: (characterId: string) => Promise<ResultT<string | null>>;
resolveIdentity?: ChatCacheIdentityResolver;
}
export async function resolveSplashLatestMessageCacheIdentity(): Promise<
string | null
> {
const result = await resolveChatCacheOwnerKey();
export async function resolveSplashLatestMessageCacheIdentity(
characterId = DEFAULT_CHARACTER_ID,
): Promise<string | null> {
const result = await resolveChatConversationKey(characterId);
return Result.isOk(result) ? result.data : null;
}
export async function loadSplashLatestMessagePreview({
cache,
characterId = DEFAULT_CHARACTER_ID,
fetchPreview = fetchSplashLatestMessagePreview,
resolveIdentity = resolveChatCacheOwnerKey,
resolveIdentity = () => resolveChatConversationKey(characterId),
}: LoadSplashLatestMessagePreviewInput): Promise<ResultT<string | null>> {
const identityResult = await resolveIdentity();
const identity = Result.isOk(identityResult) ? identityResult.data : null;
@@ -34,18 +37,18 @@ export async function loadSplashLatestMessagePreview({
if (cached) return Result.ok(cached.message);
}
const result = await fetchPreview();
const result = await fetchPreview(characterId);
if (identity && Result.isOk(result)) {
cache.set(identity, result.data);
}
return result;
}
export async function fetchSplashLatestMessagePreview(): Promise<
ResultT<string | null>
> {
export async function fetchSplashLatestMessagePreview(
characterId = DEFAULT_CHARACTER_ID,
): Promise<ResultT<string | null>> {
const repository = await loadChatRepository();
const result = await repository.getHistory(1, 0);
const result = await repository.getHistory(characterId, 1, 0);
if (Result.isErr(result)) return Result.err(result.error);
return Result.ok(getLatestSplashMessagePreview(result.data.messages));
+8 -2
View File
@@ -17,6 +17,7 @@ import { isCacheableRemoteChatMediaUrl } from "./chat_media_url";
const log = new Logger("LibChatUseCachedChatMediaUrl");
export interface UseCachedChatMediaUrlInput {
characterId: string;
messageId?: string | null;
remoteUrl?: string | null;
kind: ChatMediaKind;
@@ -36,6 +37,7 @@ interface CachedMediaUrlState {
}
export function useCachedChatMediaUrl({
characterId,
messageId,
remoteUrl,
kind,
@@ -47,7 +49,9 @@ export function useCachedChatMediaUrl({
const [failedCacheKey, setFailedCacheKey] = useState<string | null>(null);
const fallbackUrl = remoteUrl ?? "";
const stateKey =
messageId && remoteUrl ? `${messageId}:${kind}:${remoteUrl}` : "";
messageId && remoteUrl
? `${characterId}:${messageId}:${kind}:${remoteUrl}`
: "";
useEffect(() => {
if (
@@ -80,6 +84,7 @@ export function useCachedChatMediaUrl({
const resolveCachedMedia = async () => {
const cachedResult = await resolveCachedChatMediaBlob({
characterId,
messageId,
kind,
});
@@ -91,6 +96,7 @@ export function useCachedChatMediaUrl({
}
const cacheResult = await cacheRemoteChatMediaBlob({
characterId,
messageId,
kind,
remoteUrl,
@@ -114,7 +120,7 @@ export function useCachedChatMediaUrl({
return () => {
cancelled = true;
};
}, [failedCacheKey, kind, messageId, remoteUrl, stateKey]);
}, [characterId, failedCacheKey, kind, messageId, remoteUrl, stateKey]);
useEffect(() => {
return () => {