mirror of
				https://github.com/Yidadaa/ChatGPT-Next-Web.git
				synced 2025-10-31 21:59:19 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			166 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			166 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {
 | |
|   ChatSession,
 | |
|   useAccessStore,
 | |
|   useAppConfig,
 | |
|   useChatStore,
 | |
| } from "../store";
 | |
| import { useMaskStore } from "../store/mask";
 | |
| import { usePromptStore } from "../store/prompt";
 | |
| import { StoreKey } from "../constant";
 | |
| import { merge } from "./merge";
 | |
| 
 | |
| type NonFunctionKeys<T> = {
 | |
|   [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
 | |
| }[keyof T];
 | |
| type NonFunctionFields<T> = Pick<T, NonFunctionKeys<T>>;
 | |
| 
 | |
| export function getNonFunctionFileds<T extends object>(obj: T) {
 | |
|   const ret: any = {};
 | |
| 
 | |
|   Object.entries(obj).map(([k, v]) => {
 | |
|     if (typeof v !== "function") {
 | |
|       ret[k] = v;
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   return ret as NonFunctionFields<T>;
 | |
| }
 | |
| 
 | |
| export type GetStoreState<T> = T extends { getState: () => infer U }
 | |
|   ? NonFunctionFields<U>
 | |
|   : never;
 | |
| 
 | |
| const LocalStateSetters = {
 | |
|   [StoreKey.Chat]: useChatStore.setState,
 | |
|   [StoreKey.Access]: useAccessStore.setState,
 | |
|   [StoreKey.Config]: useAppConfig.setState,
 | |
|   [StoreKey.Mask]: useMaskStore.setState,
 | |
|   [StoreKey.Prompt]: usePromptStore.setState,
 | |
| } as const;
 | |
| 
 | |
| const LocalStateGetters = {
 | |
|   [StoreKey.Chat]: () => getNonFunctionFileds(useChatStore.getState()),
 | |
|   [StoreKey.Access]: () => getNonFunctionFileds(useAccessStore.getState()),
 | |
|   [StoreKey.Config]: () => getNonFunctionFileds(useAppConfig.getState()),
 | |
|   [StoreKey.Mask]: () => getNonFunctionFileds(useMaskStore.getState()),
 | |
|   [StoreKey.Prompt]: () => getNonFunctionFileds(usePromptStore.getState()),
 | |
| } as const;
 | |
| 
 | |
| export type AppState = {
 | |
|   [k in keyof typeof LocalStateGetters]: ReturnType<
 | |
|     (typeof LocalStateGetters)[k]
 | |
|   >;
 | |
| };
 | |
| 
 | |
| type Merger<T extends keyof AppState, U = AppState[T]> = (
 | |
|   localState: U,
 | |
|   remoteState: U,
 | |
| ) => U;
 | |
| 
 | |
| type StateMerger = {
 | |
|   [K in keyof AppState]: Merger<K>;
 | |
| };
 | |
| 
 | |
| // we merge remote state to local state
 | |
| const MergeStates: StateMerger = {
 | |
|   [StoreKey.Chat]: (localState, remoteState) => {
 | |
|     // merge sessions
 | |
|     const localSessions: Record<string, ChatSession> = {};
 | |
|     localState.sessions.forEach((s) => (localSessions[s.id] = s));
 | |
| 
 | |
|     remoteState.sessions.forEach((remoteSession) => {
 | |
|       // skip empty chats
 | |
|       if (remoteSession.messages.length === 0) return;
 | |
| 
 | |
|       const localSession = localSessions[remoteSession.id];
 | |
|       if (!localSession) {
 | |
|         // if remote session is new, just merge it
 | |
|         localState.sessions.push(remoteSession);
 | |
|       } else {
 | |
|         // if both have the same session id, merge the messages
 | |
|         const localMessageIds = new Set(localSession.messages.map((v) => v.id));
 | |
|         remoteSession.messages.forEach((m) => {
 | |
|           if (!localMessageIds.has(m.id)) {
 | |
|             localSession.messages.push(m);
 | |
|           }
 | |
|         });
 | |
| 
 | |
|         // sort local messages with date field in asc order
 | |
|         localSession.messages.sort(
 | |
|           (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
 | |
|         );
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     // sort local sessions with date field in desc order
 | |
|     localState.sessions.sort(
 | |
|       (a, b) =>
 | |
|         new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
 | |
|     );
 | |
| 
 | |
|     return localState;
 | |
|   },
 | |
|   [StoreKey.Prompt]: (localState, remoteState) => {
 | |
|     localState.prompts = {
 | |
|       ...remoteState.prompts,
 | |
|       ...localState.prompts,
 | |
|     };
 | |
|     return localState;
 | |
|   },
 | |
|   [StoreKey.Mask]: (localState, remoteState) => {
 | |
|     localState.masks = {
 | |
|       ...remoteState.masks,
 | |
|       ...localState.masks,
 | |
|     };
 | |
|     return localState;
 | |
|   },
 | |
|   [StoreKey.Config]: mergeWithUpdate<AppState[StoreKey.Config]>,
 | |
|   [StoreKey.Access]: mergeWithUpdate<AppState[StoreKey.Access]>,
 | |
| };
 | |
| 
 | |
| export function getLocalAppState() {
 | |
|   const appState = Object.fromEntries(
 | |
|     Object.entries(LocalStateGetters).map(([key, getter]) => {
 | |
|       return [key, getter()];
 | |
|     }),
 | |
|   ) as AppState;
 | |
| 
 | |
|   return appState;
 | |
| }
 | |
| 
 | |
| export function setLocalAppState(appState: AppState) {
 | |
|   Object.entries(LocalStateSetters).forEach(([key, setter]) => {
 | |
|     setter(appState[key as keyof AppState]);
 | |
|   });
 | |
| }
 | |
| 
 | |
| export function mergeAppState(localState: AppState, remoteState: AppState) {
 | |
|   Object.keys(localState).forEach(<T extends keyof AppState>(k: string) => {
 | |
|     const key = k as T;
 | |
|     const localStoreState = localState[key];
 | |
|     const remoteStoreState = remoteState[key];
 | |
|     MergeStates[key](localStoreState, remoteStoreState);
 | |
|   });
 | |
| 
 | |
|   return localState;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Merge state with `lastUpdateTime`, older state will be override
 | |
|  */
 | |
| export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
 | |
|   localState: T,
 | |
|   remoteState: T,
 | |
| ) {
 | |
|   const localUpdateTime = localState.lastUpdateTime ?? 0;
 | |
|   const remoteUpdateTime = localState.lastUpdateTime ?? 1;
 | |
| 
 | |
|   if (localUpdateTime < remoteUpdateTime) {
 | |
|     merge(remoteState, localState);
 | |
|     return { ...remoteState };
 | |
|   } else {
 | |
|     merge(localState, remoteState);
 | |
|     return { ...localState };
 | |
|   }
 | |
| }
 |