From 881ad717cfdc54934b7d3e49c9bb9f47979dfa8f Mon Sep 17 00:00:00 2001 From: CloudPassenger Date: Wed, 17 Jul 2024 11:26:21 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(service):=20Seper?= =?UTF-8?q?ate=20data=20import=20and=20export=20services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(mobile)/me/data/features/Category.tsx | 10 +- .../SessionListContent/List/Item/Actions.tsx | 6 +- .../chat/features/Migration/UpgradeButton.tsx | 4 +- .../chat/settings/features/HeaderContent.tsx | 6 +- src/const/version.ts | 2 + src/features/DataImporter/index.tsx | 6 +- src/features/User/UserPanel/useMenu.tsx | 10 +- .../DataImporter/__tests__/index.test.ts | 1 - src/server/modules/DataImporter/index.ts | 2 +- src/services/{config.ts => export/client.ts} | 77 +++--------- src/services/export/index.ts | 13 +++ src/services/export/server.ts | 110 ++++++++++++++++++ src/services/export/type.ts | 9 ++ src/services/export/utils.ts | 12 ++ src/services/import/client.ts | 31 ++++- src/services/import/index.ts | 8 ++ src/services/import/server.ts | 27 ++++- src/services/import/type.ts | 15 +++ 18 files changed, 259 insertions(+), 90 deletions(-) rename src/services/{config.ts => export/client.ts} (55%) create mode 100644 src/services/export/index.ts create mode 100644 src/services/export/server.ts create mode 100644 src/services/export/type.ts create mode 100644 src/services/export/utils.ts create mode 100644 src/services/import/type.ts diff --git a/src/app/(main)/(mobile)/me/data/features/Category.tsx b/src/app/(main)/(mobile)/me/data/features/Category.tsx index 174d1bf0b0f41..3211d179c136f 100644 --- a/src/app/(main)/(mobile)/me/data/features/Category.tsx +++ b/src/app/(main)/(mobile)/me/data/features/Category.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import Cell, { CellProps } from '@/components/Cell'; import DataImporter from '@/features/DataImporter'; -import { configService } from '@/services/config'; +import { exportService } from '@/services/export'; const Category = memo(() => { const { t } = useTranslation('common'); @@ -13,17 +13,17 @@ const Category = memo(() => { { key: 'allAgent', label: t('exportType.allAgent'), - onClick: configService.exportAgents, + onClick: exportService.exportAgents, }, { key: 'allAgentWithMessage', label: t('exportType.allAgentWithMessage'), - onClick: configService.exportSessions, + onClick: exportService.exportSessions, }, { key: 'globalSetting', label: t('exportType.globalSetting'), - onClick: configService.exportSettings, + onClick: exportService.exportSettings, }, { type: 'divider', @@ -31,7 +31,7 @@ const Category = memo(() => { { key: 'all', label: t('exportType.all'), - onClick: configService.exportAll, + onClick: exportService.exportAll, }, { type: 'divider', diff --git a/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx b/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx index 5b4f120c7113d..e145cd896a8ff 100644 --- a/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +++ b/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx @@ -18,7 +18,7 @@ import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { isServerMode } from '@/const/version'; -import { configService } from '@/services/config'; +import { exportService } from '@/services/export'; import { useSessionStore } from '@/store/session'; import { sessionHelpers } from '@/store/session/helpers'; import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors'; @@ -131,14 +131,14 @@ const Actions = memo(({ group, id, openCreateGroupModal, setOpen }) key: 'agent', label: t('exportType.agent', { ns: 'common' }), onClick: () => { - configService.exportSingleAgent(id); + exportService.exportSingleAgent(id); }, }, { key: 'agentWithMessage', label: t('exportType.agentWithMessage', { ns: 'common' }), onClick: () => { - configService.exportSingleSession(id); + exportService.exportSingleSession(id); }, }, ], diff --git a/src/app/(main)/chat/features/Migration/UpgradeButton.tsx b/src/app/(main)/chat/features/Migration/UpgradeButton.tsx index c342e18a6151a..590cb8a464e7d 100644 --- a/src/app/(main)/chat/features/Migration/UpgradeButton.tsx +++ b/src/app/(main)/chat/features/Migration/UpgradeButton.tsx @@ -4,7 +4,7 @@ import { ReactNode, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Migration } from '@/migrations'; -import { configService } from '@/services/config'; +import { importService } from '@/services/import'; import { useChatStore } from '@/store/chat'; import { useSessionStore } from '@/store/session'; @@ -35,7 +35,7 @@ const UpgradeButton = memo( setUpgradeStatus(UpgradeStatus.UPGRADING); - await configService.importConfigState({ + await importService.importConfigState({ exportType: 'sessions', state: data.state, version: 2, diff --git a/src/app/(main)/chat/settings/features/HeaderContent.tsx b/src/app/(main)/chat/settings/features/HeaderContent.tsx index 9238394f8f921..6ff5e89acc3c9 100644 --- a/src/app/(main)/chat/settings/features/HeaderContent.tsx +++ b/src/app/(main)/chat/settings/features/HeaderContent.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { HEADER_ICON_SIZE } from '@/const/layoutTokens'; import { isServerMode } from '@/const/version'; -import { configService } from '@/services/config'; +import { exportService } from '@/services/export'; import { useServerConfigStore } from '@/store/serverConfig'; import { useSessionStore } from '@/store/session'; @@ -29,7 +29,7 @@ export const HeaderContent = memo<{ mobile?: boolean; modal?: boolean }>(({ moda onClick: () => { if (!id) return; - configService.exportSingleAgent(id); + exportService.exportSingleAgent(id); }, }, { @@ -38,7 +38,7 @@ export const HeaderContent = memo<{ mobile?: boolean; modal?: boolean }>(({ moda onClick: () => { if (!id) return; - configService.exportSingleSession(id); + exportService.exportSingleSession(id); }, }, ], diff --git a/src/const/version.ts b/src/const/version.ts index c3ab173d7e0f6..f9d3b993035e0 100644 --- a/src/const/version.ts +++ b/src/const/version.ts @@ -4,3 +4,5 @@ import { getServerDBConfig } from '@/config/db'; export const CURRENT_VERSION = pkg.version; export const isServerMode = getServerDBConfig().NEXT_PUBLIC_ENABLED_SERVER_SERVICE; + +// 检查是否存在配置遗留 diff --git a/src/features/DataImporter/index.tsx b/src/features/DataImporter/index.tsx index 1913194f8cce1..4f26fefc5b386 100644 --- a/src/features/DataImporter/index.tsx +++ b/src/features/DataImporter/index.tsx @@ -8,10 +8,10 @@ import { useTranslation } from 'react-i18next'; import { Center } from 'react-layout-kit'; import DataStyleModal from '@/components/DataStyleModal'; -import { ImportResult, ImportResults, configService } from '@/services/config'; +import { ImportResult, importService } from '@/services/import'; import { useChatStore } from '@/store/chat'; import { useSessionStore } from '@/store/session'; -import { ErrorShape, FileUploadState, ImportStage } from '@/types/importer'; +import { ErrorShape, FileUploadState, ImportResults, ImportStage } from '@/types/importer'; import { importConfigFile } from '@/utils/config'; import ImportError from './Error'; @@ -148,7 +148,7 @@ const DataImporter = memo(({ children, onFinishImport }) => { await importConfigFile(file, async (config) => { setImportState(ImportStage.Preparing); - await configService.importConfigState(config, { + await importService.importConfigState(config, { onError: (error) => { setImportError(error); }, diff --git a/src/features/User/UserPanel/useMenu.tsx b/src/features/User/UserPanel/useMenu.tsx index 454fc22203d6d..dc21c0f492936 100644 --- a/src/features/User/UserPanel/useMenu.tsx +++ b/src/features/User/UserPanel/useMenu.tsx @@ -36,7 +36,7 @@ import DataImporter from '@/features/DataImporter'; import { useOpenSettings } from '@/hooks/useInterceptingRoutes'; import { usePWAInstall } from '@/hooks/usePWAInstall'; import { useQueryRoute } from '@/hooks/useQueryRoute'; -import { configService } from '@/services/config'; +import { exportService } from '@/services/export'; import { SettingsTabs } from '@/store/global/initialState'; import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig'; import { useUserStore } from '@/store/user'; @@ -143,17 +143,17 @@ export const useMenu = () => { { key: 'allAgent', label: t('exportType.allAgent'), - onClick: configService.exportAgents, + onClick: exportService.exportAgents, }, { key: 'allAgentWithMessage', label: t('exportType.allAgentWithMessage'), - onClick: configService.exportSessions, + onClick: exportService.exportSessions, }, { key: 'globalSetting', label: t('exportType.globalSetting'), - onClick: configService.exportSettings, + onClick: exportService.exportSettings, }, { type: 'divider', @@ -161,7 +161,7 @@ export const useMenu = () => { { key: 'all', label: t('exportType.all'), - onClick: configService.exportAll, + onClick: exportService.exportAll, }, ], icon: , diff --git a/src/server/modules/DataImporter/__tests__/index.test.ts b/src/server/modules/DataImporter/__tests__/index.test.ts index 55f4570c7448a..03734edc0412e 100644 --- a/src/server/modules/DataImporter/__tests__/index.test.ts +++ b/src/server/modules/DataImporter/__tests__/index.test.ts @@ -13,7 +13,6 @@ import { users, } from '@/database/server/schemas/lobechat'; import { CURRENT_CONFIG_VERSION } from '@/migrations'; -import { ImportResult } from '@/services/config'; import { ImporterEntryData } from '@/types/importer'; import { DataImporter } from '../index'; diff --git a/src/server/modules/DataImporter/index.ts b/src/server/modules/DataImporter/index.ts index f02727679022e..0319333870709 100644 --- a/src/server/modules/DataImporter/index.ts +++ b/src/server/modules/DataImporter/index.ts @@ -12,7 +12,7 @@ import { sessions, topics, } from '@/database/server/schemas/lobechat'; -import { ImportResult } from '@/services/config'; +import { ImportResult } from '@/services/import'; import { ImporterEntryData } from '@/types/importer'; export class DataImporter { diff --git a/src/services/config.ts b/src/services/export/client.ts similarity index 55% rename from src/services/config.ts rename to src/services/export/client.ts index 3473cba6312cf..a7f12525290b6 100644 --- a/src/services/config.ts +++ b/src/services/export/client.ts @@ -1,54 +1,15 @@ -import { importService } from '@/services/import'; -import { messageService } from '@/services/message'; -import { sessionService } from '@/services/session'; -import { topicService } from '@/services/topic'; -import { useSessionStore } from '@/store/session'; -import { sessionSelectors } from '@/store/session/selectors'; -import { useUserStore } from '@/store/user'; -import { settingsSelectors } from '@/store/user/selectors'; -import { ConfigFile } from '@/types/exportConfig'; -import { ImportStage, OnImportCallbacks } from '@/types/importer'; +import { IExportService } from '@/services/export/type'; +import { getAgent, getSession, getSettings } from '@/services/export/utils'; +import { ClientService as MessageService } from '@/services/message/client'; +import { ClientService as SessionService } from '@/services/session/client'; +import { ClientService as TopicService } from '@/services/topic/client'; import { createConfigFile, exportConfigFile } from '@/utils/config'; -export interface ImportResult { - added: number; - errors: number; - skips: number; -} -export interface ImportResults { - messages?: ImportResult; - sessionGroups?: ImportResult; - sessions?: ImportResult; - topics?: ImportResult; - type?: string; -} - -class ConfigService { - importConfigState = async (config: ConfigFile, callbacks?: OnImportCallbacks): Promise => { - if (config.exportType === 'settings') { - await importService.importSettings(config.state.settings); - callbacks?.onStageChange?.(ImportStage.Success); - return; - } - - if (config.exportType === 'all') { - await importService.importSettings(config.state.settings); - } - - await importService.importData( - { - messages: (config.state as any).messages || [], - sessionGroups: (config.state as any).sessionGroups || [], - sessions: (config.state as any).sessions || [], - topics: (config.state as any).topics || [], - version: config.version, - }, - callbacks, - ); - }; - - // TODO: Separate export feature into a new service like importService +const sessionService = new SessionService(); +const messageService = new MessageService(); +const topicService = new TopicService(); +export class ClientService implements IExportService { /** * export all agents */ @@ -79,7 +40,7 @@ class ConfigService { * export a session */ exportSingleSession = async (id: string) => { - const session = this.getSession(id); + const session = getSession(id); if (!session) return; const messages = await messageService.getAllMessagesInSession(id); @@ -94,7 +55,7 @@ class ConfigService { * export a topic */ exportSingleTopic = async (sessionId: string, topicId: string) => { - const session = this.getSession(sessionId); + const session = getSession(sessionId); if (!session) return; const messages = await messageService.getMessages(sessionId, topicId); @@ -113,7 +74,7 @@ class ConfigService { }; exportSingleAgent = async (id: string) => { - const agent = this.getAgent(id); + const agent = getAgent(id); if (!agent) return; const config = createConfigFile('agents', { sessionGroups: [], sessions: [agent] }); @@ -125,7 +86,7 @@ class ConfigService { * export settings */ exportSettings = async () => { - const settings = this.getSettings(); + const settings = getSettings(); const config = createConfigFile('settings', { settings }); @@ -140,20 +101,10 @@ class ConfigService { const sessionGroups = await sessionService.getSessionGroups(); const messages = await messageService.getAllMessages(); const topics = await topicService.getAllTopics(); - const settings = this.getSettings(); + const settings = getSettings(); const config = createConfigFile('all', { messages, sessionGroups, sessions, settings, topics }); exportConfigFile(config, 'config'); }; - - private getSettings = () => settingsSelectors.exportSettings(useUserStore.getState()); - - private getSession = (id: string) => - sessionSelectors.getSessionById(id)(useSessionStore.getState()); - - private getAgent = (id: string) => - sessionSelectors.getSessionById(id)(useSessionStore.getState()); } - -export const configService = new ConfigService(); diff --git a/src/services/export/index.ts b/src/services/export/index.ts new file mode 100644 index 0000000000000..ebee0a16ebeaf --- /dev/null +++ b/src/services/export/index.ts @@ -0,0 +1,13 @@ +import { isServerMode } from '@/const/version'; + +import { ClientService } from './client'; +import { ServerService } from './server'; + +export const exportService = isServerMode ? new ServerService() : new ClientService(); + +// TODO: A new UI to guide users in migrating personal data from Local DB to Server DB +export const getExportService = (mode?: 'client' | 'server') => { + if (mode === 'client') return new ClientService(); + if (mode === 'server') return new ServerService(); + return exportService; +}; diff --git a/src/services/export/server.ts b/src/services/export/server.ts new file mode 100644 index 0000000000000..d3cc3b868e098 --- /dev/null +++ b/src/services/export/server.ts @@ -0,0 +1,110 @@ +import { IExportService } from '@/services/export/type'; +import { getAgent, getSession, getSettings } from '@/services/export/utils'; +import { ServerService as MessageService } from '@/services/message/server'; +import { ServerService as SessionService } from '@/services/session/server'; +import { ServerService as TopicService } from '@/services/topic/server'; +import { createConfigFile, exportConfigFile } from '@/utils/config'; + +const sessionService = new SessionService(); +const messageService = new MessageService(); +const topicService = new TopicService(); + +export class ServerService implements IExportService { + /** + * export all agents + */ + exportAgents = async () => { + const agents = await sessionService.getSessionsByType('agent'); + const sessionGroups = await sessionService.getSessionGroups(); + + const config = createConfigFile('agents', { sessionGroups, sessions: agents }); + + exportConfigFile(config, 'agents'); + }; + + /** + * export all sessions + */ + exportSessions = async () => { + const sessions = await sessionService.getSessionsByType(); + const sessionGroups = await sessionService.getSessionGroups(); + const messages = await messageService.getAllMessages(); + const topics = await topicService.getAllTopics(); + + const config = createConfigFile('sessions', { messages, sessionGroups, sessions, topics }); + + exportConfigFile(config, 'sessions'); + }; + + /** + * export a session + */ + exportSingleSession = async (id: string) => { + const session = getSession(id); + if (!session) return; + + const messages = await messageService.getAllMessagesInSession(id); + const topics = await topicService.getTopics({ sessionId: id }); + + const config = createConfigFile('singleSession', { messages, sessions: [session], topics }); + + exportConfigFile(config, `${session.meta?.title}-session`); + }; + + /** + * export a topic + */ + exportSingleTopic = async (sessionId: string, topicId: string) => { + const session = getSession(sessionId); + if (!session) return; + + const messages = await messageService.getMessages(sessionId, topicId); + const topics = await topicService.getTopics({ sessionId }); + + const topic = topics.find((item) => item.id === topicId); + if (!topic) return; + + const config = createConfigFile('singleSession', { + messages, + sessions: [session], + topics: [topic], + }); + + exportConfigFile(config, `${topic.title}-topic`); + }; + + exportSingleAgent = async (id: string) => { + const agent = getAgent(id); + if (!agent) return; + + const config = createConfigFile('agents', { sessionGroups: [], sessions: [agent] }); + + exportConfigFile(config, agent.meta?.title || 'agent'); + }; + + /** + * export settings + */ + exportSettings = async () => { + const settings = getSettings(); + + const config = createConfigFile('settings', { settings }); + + exportConfigFile(config, 'settings'); + }; + + /** + * export all data + */ + exportAll = async () => { + const sessions = await sessionService.getSessionsByType(); + const sessionGroups = await sessionService.getSessionGroups(); + const messages = await messageService.getAllMessages(); + const topics = await topicService.getAllTopics(); + const settings = getSettings(); + + const config = createConfigFile('all', { messages, sessionGroups, sessions, settings, topics }); + + exportConfigFile(config, 'config'); + }; +} diff --git a/src/services/export/type.ts b/src/services/export/type.ts new file mode 100644 index 0000000000000..c649fca3c784b --- /dev/null +++ b/src/services/export/type.ts @@ -0,0 +1,9 @@ +export interface IExportService { + exportAgents(): Promise; + exportAll(): Promise; + exportSessions(): Promise; + exportSettings(): Promise; + exportSingleAgent(id: string): Promise; + exportSingleSession(id: string): Promise; + exportSingleTopic(sessionId: string, topicId: string): Promise; +} diff --git a/src/services/export/utils.ts b/src/services/export/utils.ts new file mode 100644 index 0000000000000..f1ce6ca4ac9de --- /dev/null +++ b/src/services/export/utils.ts @@ -0,0 +1,12 @@ +import { useSessionStore } from '@/store/session'; +import { sessionSelectors } from '@/store/session/selectors'; +import { useUserStore } from '@/store/user'; +import { settingsSelectors } from '@/store/user/selectors'; + +export const getSession = (id: string) => + sessionSelectors.getSessionById(id)(useSessionStore.getState()); + +export const getAgent = (id: string) => + sessionSelectors.getSessionById(id)(useSessionStore.getState()); + +export const getSettings = () => settingsSelectors.exportSettings(useUserStore.getState()); diff --git a/src/services/import/client.ts b/src/services/import/client.ts index 274e3b81028f1..ac2a58f28098c 100644 --- a/src/services/import/client.ts +++ b/src/services/import/client.ts @@ -2,12 +2,37 @@ import { MessageModel } from '@/database/client/models/message'; import { SessionModel } from '@/database/client/models/session'; import { SessionGroupModel } from '@/database/client/models/sessionGroup'; import { TopicModel } from '@/database/client/models/topic'; -import { ImportResult, ImportResults } from '@/services/config'; +import { ImportResult } from '@/services/import'; +import { IImportService } from '@/services/import/type'; import { useUserStore } from '@/store/user'; -import { ImportStage, ImporterEntryData, OnImportCallbacks } from '@/types/importer'; +import { ConfigFile } from '@/types/exportConfig'; +import { ImportResults, ImportStage, ImporterEntryData, OnImportCallbacks } from '@/types/importer'; import { UserSettings } from '@/types/user/settings'; -export class ClientService { +export class ClientService implements IImportService { + importConfigState = async (config: ConfigFile, callbacks?: OnImportCallbacks): Promise => { + if (config.exportType === 'settings') { + await this.importSettings(config.state.settings); + callbacks?.onStageChange?.(ImportStage.Success); + return; + } + + if (config.exportType === 'all') { + await this.importSettings(config.state.settings); + } + + await this.importData( + { + messages: (config.state as any).messages || [], + sessionGroups: (config.state as any).sessionGroups || [], + sessions: (config.state as any).sessions || [], + topics: (config.state as any).topics || [], + version: config.version, + }, + callbacks, + ); + }; + importSettings = async (settings: UserSettings) => { await useUserStore.getState().importAppSettings(settings); }; diff --git a/src/services/import/index.ts b/src/services/import/index.ts index 2400e7b8c8e3d..5322440a48949 100644 --- a/src/services/import/index.ts +++ b/src/services/import/index.ts @@ -3,4 +3,12 @@ import { isServerMode } from '@/const/version'; import { ClientService } from './client'; import { ServerService } from './server'; +export type { ImportResult } from './type'; + export const importService = isServerMode ? new ServerService() : new ClientService(); + +export const getImportService = (mode?: 'client' | 'server') => { + if (mode === 'client') return new ClientService(); + if (mode === 'server') return new ServerService(); + return importService; +}; diff --git a/src/services/import/server.ts b/src/services/import/server.ts index 14d35acc0b8a8..5ee697b2a0966 100644 --- a/src/services/import/server.ts +++ b/src/services/import/server.ts @@ -1,12 +1,37 @@ import { DefaultErrorShape } from '@trpc/server/unstable-core-do-not-import'; import { edgeClient, lambdaClient } from '@/libs/trpc/client'; +import { IImportService } from '@/services/import/type'; import { useUserStore } from '@/store/user'; +import { ConfigFile } from '@/types/exportConfig'; import { ImportStage, ImporterEntryData, OnImportCallbacks } from '@/types/importer'; import { UserSettings } from '@/types/user/settings'; import { uuid } from '@/utils/uuid'; -export class ServerService { +export class ServerService implements IImportService { + importConfigState = async (config: ConfigFile, callbacks?: OnImportCallbacks): Promise => { + if (config.exportType === 'settings') { + await this.importSettings(config.state.settings); + callbacks?.onStageChange?.(ImportStage.Success); + return; + } + + if (config.exportType === 'all') { + await this.importSettings(config.state.settings); + } + + await this.importData( + { + messages: (config.state as any).messages || [], + sessionGroups: (config.state as any).sessionGroups || [], + sessions: (config.state as any).sessions || [], + topics: (config.state as any).topics || [], + version: config.version, + }, + callbacks, + ); + }; + importSettings = async (settings: UserSettings) => { await useUserStore.getState().importAppSettings(settings); }; diff --git a/src/services/import/type.ts b/src/services/import/type.ts new file mode 100644 index 0000000000000..21da1feb4b038 --- /dev/null +++ b/src/services/import/type.ts @@ -0,0 +1,15 @@ +import { ConfigFile } from '@/types/exportConfig'; +import { ImportResults, ImporterEntryData, OnImportCallbacks } from '@/types/importer'; +import { UserSettings } from '@/types/user/settings'; + +export interface IImportService { + importConfigState(config: ConfigFile, callbacks?: OnImportCallbacks): Promise; + importData(data: ImporterEntryData, callbacks?: OnImportCallbacks): Promise; + importSettings(settings: UserSettings): Promise; +} + +export interface ImportResult { + added: number; + errors: number; + skips: number; +}