From 4d1c85727a78ab88b8ea3507573b1773a5055e5a Mon Sep 17 00:00:00 2001 From: chenhang Date: Mon, 20 Jul 2026 14:02:39 +0800 Subject: [PATCH] refactor(chat): remove guest history synchronization --- .../chat/requests/sync-guest-messages.json | 15 ---- .../repositories/chat_remote_data_source.ts | 11 --- src/data/repositories/chat_repository.ts | 8 -- .../interfaces/ichat_repository.ts | 7 -- .../__tests__/multi_role_contract.test.ts | 14 +--- src/data/schemas/chat/index.ts | 1 - .../schemas/chat/request/chat_sync_request.ts | 19 ----- src/data/schemas/chat/request/index.ts | 1 - .../api/__tests__/multi_character_api.test.ts | 27 ++---- src/data/services/api/api_contract.json | 3 +- src/data/services/api/api_path.ts | 2 - src/data/services/api/chat_api.ts | 12 --- .../storage/navigation/navigation_storage.ts | 16 ---- src/data/storage/storage_keys.ts | 1 - .../chat/__tests__/guest-history-sync.test.ts | 84 ------------------- src/stores/chat/guest-history-sync.ts | 65 -------------- src/stores/sync/chat-auth-sync.tsx | 24 +----- 17 files changed, 10 insertions(+), 300 deletions(-) delete mode 100644 src/data/mock/chat/requests/sync-guest-messages.json delete mode 100644 src/data/schemas/chat/request/chat_sync_request.ts delete mode 100644 src/stores/chat/__tests__/guest-history-sync.test.ts delete mode 100644 src/stores/chat/guest-history-sync.ts diff --git a/src/data/mock/chat/requests/sync-guest-messages.json b/src/data/mock/chat/requests/sync-guest-messages.json deleted file mode 100644 index 5df17fc0..00000000 --- a/src/data/mock/chat/requests/sync-guest-messages.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "characterId": "elio", - "messages": [ - { - "role": "user", - "content": "Hi Elio.", - "timestamp": "2026-06-23T02:10:00.000Z" - }, - { - "role": "assistant", - "content": "Hi, I am right here with you.", - "timestamp": "2026-06-23T02:10:04.000Z" - } - ] -} diff --git a/src/data/repositories/chat_remote_data_source.ts b/src/data/repositories/chat_remote_data_source.ts index d1b39541..4555a64d 100644 --- a/src/data/repositories/chat_remote_data_source.ts +++ b/src/data/repositories/chat_remote_data_source.ts @@ -6,8 +6,6 @@ import type { import { ChatHistoryResponse, ChatPreviewsResponse, - ChatSyncRequest, - ChatSyncRequestSchema, ChatSendResponse, SendMessageRequestSchema, UnlockHistoryRequestSchema, @@ -58,15 +56,6 @@ export class ChatRemoteDataSource { return Result.wrap(() => this.api.getPreviews(options)); } - async syncGuestHistory( - request: ChatSyncRequest, - options?: ChatRequestOptions, - ): Promise> { - return Result.wrap(() => - this.api.syncGuestHistory(ChatSyncRequestSchema.parse(request), options), - ); - } - async getHistory( characterId: string, limit = 50, diff --git a/src/data/repositories/chat_repository.ts b/src/data/repositories/chat_repository.ts index e900c3fb..979818ec 100644 --- a/src/data/repositories/chat_repository.ts +++ b/src/data/repositories/chat_repository.ts @@ -10,7 +10,6 @@ import type { import type { ChatHistoryResponse, ChatPreviewsResponse, - ChatSyncRequest, ChatMessage, ChatSendResponse, UnlockHistoryResponse, @@ -63,13 +62,6 @@ export class ChatRepository implements IChatRepository { return this.remote.getPreviews(options); } - async syncGuestHistory( - request: ChatSyncRequest, - options?: ChatRequestOptions, - ): Promise> { - return this.remote.syncGuestHistory(request, options); - } - /** 解锁单条历史付费 / 私密消息。 */ async unlockPrivateMessage( input: UnlockPrivateMessageInput, diff --git a/src/data/repositories/interfaces/ichat_repository.ts b/src/data/repositories/interfaces/ichat_repository.ts index b318e1d8..71feba2e 100644 --- a/src/data/repositories/interfaces/ichat_repository.ts +++ b/src/data/repositories/interfaces/ichat_repository.ts @@ -7,7 +7,6 @@ import type { ChatHistoryResponse, ChatPreviewsResponse, - ChatSyncRequest, ChatImageData, ChatLockDetailData, ChatLockType, @@ -79,12 +78,6 @@ export interface IChatRepository { options?: ChatRequestOptions, ): Promise>; - /** 把一个角色的游客历史同步到正式账号。 */ - syncGuestHistory( - request: ChatSyncRequest, - options?: ChatRequestOptions, - ): Promise>; - /** 解锁单条历史付费 / 私密消息。 */ unlockPrivateMessage( input: UnlockPrivateMessageInput, diff --git a/src/data/schemas/chat/__tests__/multi_role_contract.test.ts b/src/data/schemas/chat/__tests__/multi_role_contract.test.ts index ba5862b0..0bc4e0ce 100644 --- a/src/data/schemas/chat/__tests__/multi_role_contract.test.ts +++ b/src/data/schemas/chat/__tests__/multi_role_contract.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest"; import { ChatPreviewsResponseSchema, - ChatSyncRequestSchema, SendMessageRequestSchema, } from "@/data/schemas/chat"; @@ -26,23 +25,12 @@ describe("multi-role chat schemas", () => { ).toThrow(); }); - it("parses nullable previews and immutable sync messages", () => { + it("parses nullable immutable previews", () => { const previews = ChatPreviewsResponseSchema.parse({ items: [{ characterId: "elio", message: null }], }); - const sync = ChatSyncRequestSchema.parse({ - characterId: "elio", - messages: [ - { - role: "assistant", - content: "Welcome back", - timestamp: "2026-07-20T00:00:00.000Z", - }, - ], - }); expect(previews.items[0]?.message).toBeNull(); expect(Object.isFrozen(previews.items)).toBe(true); - expect(Object.isFrozen(sync.messages)).toBe(true); }); }); diff --git a/src/data/schemas/chat/index.ts b/src/data/schemas/chat/index.ts index a4613d0f..9984d69f 100644 --- a/src/data/schemas/chat/index.ts +++ b/src/data/schemas/chat/index.ts @@ -7,7 +7,6 @@ export * from "./chat_media"; export * from "./chat_message"; export * from "./chat_payloads"; export * from "./request/send_message_request"; -export * from "./request/chat_sync_request"; export * from "./request/unlock_history_request"; export * from "./request/unlock_private_request"; export * from "./response/chat_history_response"; diff --git a/src/data/schemas/chat/request/chat_sync_request.ts b/src/data/schemas/chat/request/chat_sync_request.ts deleted file mode 100644 index c5e5566f..00000000 --- a/src/data/schemas/chat/request/chat_sync_request.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod"; - -export const ChatSyncMessageSchema = z - .object({ - role: z.enum(["user", "assistant"]), - content: z.string(), - timestamp: z.string().min(1), - }) - .readonly(); - -export const ChatSyncRequestSchema = z - .object({ - characterId: z.string().min(1), - messages: z.array(ChatSyncMessageSchema).min(1).readonly(), - }) - .readonly(); - -export type ChatSyncRequestInput = z.input; -export type ChatSyncRequest = z.output; diff --git a/src/data/schemas/chat/request/index.ts b/src/data/schemas/chat/request/index.ts index 1a375418..c7462863 100644 --- a/src/data/schemas/chat/request/index.ts +++ b/src/data/schemas/chat/request/index.ts @@ -3,6 +3,5 @@ */ export * from "./send_message_request"; -export * from "./chat_sync_request"; export * from "./unlock_private_request"; export * from "./unlock_history_request"; diff --git a/src/data/services/api/__tests__/multi_character_api.test.ts b/src/data/services/api/__tests__/multi_character_api.test.ts index ca76475a..18fd1906 100644 --- a/src/data/services/api/__tests__/multi_character_api.test.ts +++ b/src/data/services/api/__tests__/multi_character_api.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { - ChatSyncRequestSchema, SendMessageRequestSchema, UnlockHistoryRequestSchema, UnlockPrivateRequestSchema, @@ -74,30 +73,16 @@ describe("multi-character API contract", () => { }); }); - it("loads previews and syncs guest history", async () => { + it("loads chat previews", async () => { const api = new ChatApi(); - httpClientMock - .mockResolvedValueOnce({ success: true, data: { items: [] } }) - .mockResolvedValueOnce({ success: true, data: {} }); + httpClientMock.mockResolvedValueOnce({ + success: true, + data: { items: [] }, + }); await api.getPreviews(); - const request = ChatSyncRequestSchema.parse({ - characterId: CHARACTER_ID, - messages: [ - { - role: "user", - content: "Hello", - timestamp: "2026-07-20T00:00:00.000Z", - }, - ], - }); - await api.syncGuestHistory(request); - expect(httpClientMock).toHaveBeenNthCalledWith(1, "/api/chat/previews", {}); - expect(httpClientMock).toHaveBeenNthCalledWith(2, "/api/chat/sync", { - method: "POST", - body: request, - }); + expect(httpClientMock).toHaveBeenCalledWith("/api/chat/previews", {}); }); it("forwards request cancellation to the HTTP client", async () => { diff --git a/src/data/services/api/api_contract.json b/src/data/services/api/api_contract.json index da1e9855..ae01de69 100644 --- a/src/data/services/api/api_contract.json +++ b/src/data/services/api/api_contract.json @@ -26,6 +26,5 @@ "reportUserInfo": { "method": "post", "path": "/api/data/report-user-info" }, "feedback": { "method": "post", "path": "/api/feedback" }, "characters": { "method": "get", "path": "/api/characters" }, - "chatPreviews": { "method": "get", "path": "/api/chat/previews" }, - "chatSync": { "method": "post", "path": "/api/chat/sync" } + "chatPreviews": { "method": "get", "path": "/api/chat/previews" } } diff --git a/src/data/services/api/api_path.ts b/src/data/services/api/api_path.ts index 81d79584..f26c6316 100644 --- a/src/data/services/api/api_path.ts +++ b/src/data/services/api/api_path.ts @@ -104,6 +104,4 @@ export class ApiPath { static readonly chatPreviews = apiContract.chatPreviews.path; - static readonly chatSync = apiContract.chatSync.path; - } diff --git a/src/data/services/api/chat_api.ts b/src/data/services/api/chat_api.ts index 2929b391..9dc93530 100644 --- a/src/data/services/api/chat_api.ts +++ b/src/data/services/api/chat_api.ts @@ -11,7 +11,6 @@ import { ChatPreviewsResponseSchema, ChatSendResponse, ChatSendResponseSchema, - ChatSyncRequest, SendMessageRequest, UnlockHistoryRequest, UnlockHistoryResponse, @@ -67,17 +66,6 @@ export class ChatApi { return ChatPreviewsResponseSchema.parse(unwrap(env)); } - async syncGuestHistory( - body: ChatSyncRequest, - options?: { signal?: AbortSignal }, - ): Promise { - await httpClient>(ApiPath.chatSync, { - method: "POST", - body, - ...(options?.signal ? { signal: options.signal } : {}), - }).then(unwrap); - } - /** * 解锁单条历史付费 / 私密消息 */ diff --git a/src/data/storage/navigation/navigation_storage.ts b/src/data/storage/navigation/navigation_storage.ts index 8d1904b1..49830de7 100644 --- a/src/data/storage/navigation/navigation_storage.ts +++ b/src/data/storage/navigation/navigation_storage.ts @@ -259,22 +259,6 @@ export class NavigationStorage { await SessionAsyncUtil.remove(StorageKeys.pendingChatImageReturn); } - static async saveGuestChatOwnerKey(ownerKey: string): Promise { - await SessionAsyncUtil.setJson( - StorageKeys.guestChatOwnerKey, - ownerKey, - z.string().min(1), - ); - } - - static async getGuestChatOwnerKey(): Promise { - const result = await SessionAsyncUtil.getJson( - StorageKeys.guestChatOwnerKey, - z.string().min(1), - ); - return Result.isOk(result) ? result.data : null; - } - private static parsePendingChatUnlock( value: PendingChatUnlock | null, ): PendingChatUnlock | null { diff --git a/src/data/storage/storage_keys.ts b/src/data/storage/storage_keys.ts index 7ac1963b..ad13632c 100644 --- a/src/data/storage/storage_keys.ts +++ b/src/data/storage/storage_keys.ts @@ -28,7 +28,6 @@ export const StorageKeys = { pendingChatImageReturn: "pending_chat_image_return", pendingChatUnlock: "pending_chat_unlock", pendingChatPromotion: "pending_chat_promotion", - guestChatOwnerKey: "guest_chat_owner_key", // pwa / app info pwaDialogShown: "pwa_dialog_shown", diff --git a/src/stores/chat/__tests__/guest-history-sync.test.ts b/src/stores/chat/__tests__/guest-history-sync.test.ts deleted file mode 100644 index d2850c3e..00000000 --- a/src/stores/chat/__tests__/guest-history-sync.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { Result } from "@/utils/result"; - -const mocks = vi.hoisted(() => ({ - getDeviceId: vi.fn(), - getLocalMessages: vi.fn(), - syncGuestHistory: vi.fn(), - clearLocalMessages: vi.fn(), - getGuestChatOwnerKey: vi.fn(), -})); - -vi.mock("@/data/storage/auth", () => ({ - AuthStorage: { - getInstance: () => ({ getDeviceId: mocks.getDeviceId }), - }, -})); - -vi.mock("@/data/repositories/chat_repository_loader", () => ({ - loadChatRepository: async () => ({ - getLocalMessages: mocks.getLocalMessages, - syncGuestHistory: mocks.syncGuestHistory, - clearLocalMessages: mocks.clearLocalMessages, - }), -})); - -vi.mock("@/data/storage/navigation", () => ({ - NavigationStorage: { - getGuestChatOwnerKey: mocks.getGuestChatOwnerKey, - }, -})); - -import { syncGuestHistoriesToUser } from "../guest-history-sync"; - -describe("syncGuestHistoriesToUser", () => { - beforeEach(() => { - vi.clearAllMocks(); - mocks.getDeviceId.mockResolvedValue(Result.ok("device-1")); - mocks.getGuestChatOwnerKey.mockResolvedValue(null); - mocks.clearLocalMessages.mockResolvedValue(Result.ok(undefined)); - }); - - it("groups guest history by character and clears only successful caches", async () => { - mocks.getLocalMessages.mockImplementation(async (identity: string) => - Result.ok([ - { - id: `${identity}:message`, - role: "user", - type: "text", - content: identity, - createdAt: "2026-07-20T00:00:00.000Z", - }, - ]), - ); - mocks.syncGuestHistory.mockImplementation( - async (request: { characterId: string }) => - request.characterId === "elio" - ? Result.ok(undefined) - : Result.err(new Error("offline")), - ); - - await syncGuestHistoriesToUser(["elio", "maya-tan"]); - - expect(mocks.syncGuestHistory).toHaveBeenCalledTimes(2); - expect(mocks.syncGuestHistory).toHaveBeenCalledWith( - expect.objectContaining({ characterId: "elio" }), - ); - expect(mocks.syncGuestHistory).toHaveBeenCalledWith( - expect.objectContaining({ characterId: "maya-tan" }), - ); - expect(mocks.clearLocalMessages).toHaveBeenCalledOnce(); - expect(mocks.clearLocalMessages).toHaveBeenCalledWith( - "device:device-1::character:elio", - ); - }); - - it("skips characters without guest messages", async () => { - mocks.getLocalMessages.mockResolvedValue(Result.ok([])); - - await syncGuestHistoriesToUser(["elio"]); - - expect(mocks.syncGuestHistory).not.toHaveBeenCalled(); - }); -}); diff --git a/src/stores/chat/guest-history-sync.ts b/src/stores/chat/guest-history-sync.ts deleted file mode 100644 index 4022735e..00000000 --- a/src/stores/chat/guest-history-sync.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { AuthStorage } from "@/data/storage/auth"; -import { NavigationStorage } from "@/data/storage/navigation"; -import { loadChatRepository } from "@/data/repositories/chat_repository_loader"; -import { ChatSyncRequestSchema } from "@/data/schemas/chat"; -import { buildChatConversationKey } from "@/lib/chat/chat_cache_keys"; -import { Logger } from "@/utils/logger"; -import { Result } from "@/utils/result"; - -const log = new Logger("StoresChatGuestHistorySync"); - -export async function syncGuestHistoriesToUser( - characterIds: readonly string[], -): Promise { - const deviceResult = await AuthStorage.getInstance().getDeviceId(); - const savedOwnerKey = await NavigationStorage.getGuestChatOwnerKey(); - const ownerKey = - savedOwnerKey ?? - (Result.isOk(deviceResult) && deviceResult.data - ? `device:${deviceResult.data}` - : null); - if (!ownerKey) return; - - const repository = await loadChatRepository(); - await Promise.allSettled( - [...new Set(characterIds)].map(async (characterId) => { - const cacheIdentity = buildChatConversationKey(ownerKey, characterId); - const messagesResult = await repository.getLocalMessages(cacheIdentity); - if (Result.isErr(messagesResult) || messagesResult.data.length === 0) return; - - const messages = messagesResult.data.flatMap((message) => { - if (message.role !== "user" && message.role !== "assistant") return []; - return [{ - role: message.role, - content: message.content, - timestamp: normalizeTimestamp(message.createdAt), - }]; - }); - if (messages.length === 0) return; - - const request = ChatSyncRequestSchema.parse({ characterId, messages }); - const syncResult = await repository.syncGuestHistory(request); - if (Result.isErr(syncResult)) { - log.warn("[chat-sync] guest history sync failed", { - characterId, - error: syncResult.error, - }); - return; - } - const clearResult = await repository.clearLocalMessages(cacheIdentity); - if (Result.isErr(clearResult)) { - log.warn("[chat-sync] guest history cleanup failed", { - characterId, - error: clearResult.error, - }); - } - }), - ); -} - -function normalizeTimestamp(value: string): string { - const parsed = new Date(value); - return Number.isNaN(parsed.getTime()) - ? new Date().toISOString() - : parsed.toISOString(); -} diff --git a/src/stores/sync/chat-auth-sync.tsx b/src/stores/sync/chat-auth-sync.tsx index ce3a94b2..93e57cb6 100644 --- a/src/stores/sync/chat-auth-sync.tsx +++ b/src/stores/sync/chat-auth-sync.tsx @@ -16,11 +16,6 @@ import { useAuthSelector, } from "@/stores/auth/auth-context"; import { useChatDispatch } from "@/stores/chat/chat-context"; -import { syncGuestHistoriesToUser } from "@/stores/chat/guest-history-sync"; -import { useCharacterCatalog } from "@/providers/character-catalog-provider"; -import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity"; -import { NavigationStorage } from "@/data/storage/navigation"; -import { Result } from "@/utils/result"; export function ChatAuthSync() { const authState = useAuthSelector( @@ -32,7 +27,6 @@ export function ChatAuthSync() { shallowEqual, ); const chatDispatch = useChatDispatch(); - const characterCatalog = useCharacterCatalog(); const prevSessionKeyRef = useRef(null); useEffect(() => { @@ -48,27 +42,14 @@ export function ChatAuthSync() { } if (authState.loginStatus === "guest") { - let cancelled = false; - void (async () => { - const ownerResult = await resolveChatCacheOwnerKey(); - if (Result.isOk(ownerResult)) { - await NavigationStorage.saveGuestChatOwnerKey(ownerResult.data); - } - if (!cancelled) chatDispatch({ type: "ChatGuestLogin" }); - })(); - return () => { - cancelled = true; - }; + chatDispatch({ type: "ChatGuestLogin" }); + return; } let cancelled = false; void (async () => { const tokenR = await AuthStorage.getInstance().getLoginToken(); if (!cancelled && tokenR.success && tokenR.data) { - await syncGuestHistoriesToUser( - characterCatalog.characters.map((character) => character.id), - ); - if (cancelled) return; chatDispatch({ type: "ChatUserLogin", token: tokenR.data, @@ -83,7 +64,6 @@ export function ChatAuthSync() { authState.hasInitialized, authState.isLoading, authState.loginStatus, - characterCatalog.characters, chatDispatch, ]);