feat: 实现单个会话和角色的导出功能

This commit is contained in:
arvinxx
2023-07-30 17:14:28 +08:00
parent 64c51253b7
commit d15a481332
12 changed files with 265 additions and 147 deletions

62
src/helpers/export.ts Normal file
View File

@@ -0,0 +1,62 @@
import { sessionSelectors, useSessionStore } from '@/store/session';
import { settingsSelectors, useSettings } from '@/store/settings';
import { createConfigFile, exportConfigFile } from '@/utils/config';
const getAgents = () => sessionSelectors.exportAgents(useSessionStore.getState());
const getAgent = (id: string) => sessionSelectors.getExportAgent(id)(useSessionStore.getState());
const getSessions = () => sessionSelectors.exportSessions(useSessionStore.getState());
const getSession = (id: string) => sessionSelectors.getSessionById(id)(useSessionStore.getState());
const getSettings = () => settingsSelectors.exportSettings(useSettings.getState());
export const exportAgents = () => {
const sessions = getAgents();
const config = createConfigFile('agents', { sessions });
exportConfigFile(config, 'agents');
};
export const exportSingleAgent = (id: string) => {
const agent = getAgent(id);
if (!agent) return;
const config = createConfigFile('agents', { sessions: { [id]: agent } });
exportConfigFile(config, agent.meta?.title || 'agent');
};
export const exportSessions = () => {
const sessions = getSessions();
const config = createConfigFile('sessions', { sessions });
exportConfigFile(config, 'sessions');
};
export const exportSingleSession = (id: string) => {
const session = getSession(id);
if (!session) return;
const sessions = { [id]: session };
const config = createConfigFile('sessions', { sessions });
exportConfigFile(config, `${session.meta?.title}-session`);
};
export const exportSettings = () => {
const settings = getSettings();
const config = createConfigFile('settings', { settings });
exportConfigFile(config, 'settings');
};
export const exportAll = () => {
const sessions = getSessions();
const settings = getSettings();
const config = createConfigFile('all', { sessions, settings });
exportConfigFile(config, 'config');
};

View File

@@ -1,69 +1,6 @@
import { transform } from 'lodash-es';
import { useMemo } from 'react';
import { shallow } from 'zustand/shallow';
import { Migration } from '@/migrations';
import { useSessionStore } from '@/store/session';
import { useSettings } from '@/store/settings';
import {
ConfigFileAgents,
ConfigFileAll,
ConfigFileSessions,
ConfigFileSettings,
} from '@/types/exportConfig';
import { exportConfigFile } from '@/utils/config';
import { exportAgents, exportAll, exportSessions, exportSettings } from '@/helpers/export';
export const useExportConfig = () => {
const [sessions] = useSessionStore((s) => [s.sessions], shallow);
const [settings] = useSettings((s) => [s.settings, s.importSettings], shallow);
const exportAgents = () => {
const config: ConfigFileAgents = {
exportType: 'agents',
state: {
sessions: transform(sessions, (result, value, key) => {
result[key] = { ...value, chats: {}, topics: {} };
}),
},
version: Migration.targetVersion,
};
exportConfigFile(config, 'agents');
};
const exportSessions = () => {
const config: ConfigFileSessions = {
exportType: 'sessions',
state: { sessions },
version: Migration.targetVersion,
};
exportConfigFile(config, 'sessions');
};
const exportSettings = () => {
const config: ConfigFileSettings = {
exportType: 'settings',
state: { settings },
version: Migration.targetVersion,
};
exportConfigFile(config, 'settings');
};
const exportAll = () => {
// 将 入参转换为 配置文件格式
const config: ConfigFileAll = {
exportType: 'all',
state: { sessions, settings },
version: Migration.targetVersion,
};
exportConfigFile(config, 'config');
};
return useMemo(
() => ({ exportAgents, exportAll, exportSessions, exportSettings }),
[sessions, settings],
);
};
export const useExportConfig = () =>
useMemo(() => ({ exportAgents, exportAll, exportSessions, exportSettings }), []);

View File

@@ -1,4 +1,4 @@
import { ConfigState } from '@/types/exportConfig';
import { ConfigStateAll } from '@/types/exportConfig';
import { VersionController } from '@/utils/VersionController';
// 当前最新的版本号
@@ -7,7 +7,7 @@ export const CURRENT_CONFIG_VERSION = 1;
// 历史记录版本升级模块
export const ConfigMigrations = [];
export const Migration = new VersionController<ConfigState>(
export const Migration = new VersionController<ConfigStateAll>(
ConfigMigrations,
CURRENT_CONFIG_VERSION,
);

View File

@@ -1,12 +1,13 @@
import { ActionIcon, Avatar, Icon, List } from '@lobehub/ui';
import { useHover } from 'ahooks';
import { Dropdown, type MenuProps, Popconfirm, Tag } from 'antd';
import { App, Dropdown, type MenuProps, Tag } from 'antd';
import { FolderOutput, MoreVertical, Pin, PinOff, Trash } from 'lucide-react';
import { FC, memo, useMemo, useRef, useState } from 'react';
import { FC, memo, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { exportSingleAgent, exportSingleSession } from '@/helpers/export';
import { agentSelectors, chatSelectors, sessionSelectors, useSessionStore } from '@/store/session';
import { useSettings } from '@/store/settings';
@@ -24,10 +25,10 @@ interface SessionItemProps {
const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pin }) => {
const ref = useRef(null);
const isHovering = useHover(ref);
const [popOpen, setPopOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);
const { t } = useTranslation('common');
const isHighlight = isHovering || dropdownOpen;
const isHighlight = isHovering;
const { styles, theme, cx } = useStyles(isHighlight);
const [defaultModel] = useSettings((s) => [s.settings.model], shallow);
@@ -58,23 +59,31 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pi
];
}, shallow);
// TODO: 动作绑定
const { modal } = App.useApp();
const items: MenuProps['items'] = useMemo(
() => [
{
icon: <Icon icon={pin ? PinOff : Pin} />,
key: 'pin',
label: t(pin ? 'pinOff' : 'pin'),
// TODO: 动作绑定
onClick: () => {},
},
{
children: [
{
key: 'agent',
label: <div>{t('exportType.agent')}</div>,
onClick: () => {
exportSingleAgent(id);
},
},
{
key: 'agentWithMessage',
label: <div>{t('exportType.agentWithMessage')}</div>,
onClick: () => {
exportSingleSession(id);
},
},
],
icon: <Icon icon={FolderOutput} />,
@@ -82,10 +91,24 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pi
label: t('export'),
},
{
danger: true,
icon: <Icon icon={Trash} />,
key: 'delete',
label: t('delete'),
onClick: () => setPopOpen(true),
onClick: ({ domEvent }) => {
domEvent.stopPropagation();
modal.confirm({
centered: true,
okButtonProps: { danger: true },
okText: t('ok'),
onOk: () => {
removeSession(id);
},
rootClassName: styles.modalRoot,
title: t('confirmRemoveSessionItemAlert'),
});
},
},
],
[id],
@@ -123,15 +146,6 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pi
{model}
</Tag>
)}
{/*{showChatLength && (*/}
{/* <Tag*/}
{/* bordered={false}*/}
{/* style={{ color: theme.colorTextSecondary, display: 'flex', gap: 4 }}*/}
{/* >*/}
{/* <Icon icon={LucideMessageCircle} />*/}
{/* {chatLength}*/}
{/* </Tag>*/}
{/*)}*/}
</Flexbox>
)}
</Flexbox>
@@ -140,42 +154,20 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading, pi
style={{ color: theme.colorText }}
title={title}
/>
<Popconfirm
arrow={false}
cancelText={t('cancel')}
okButtonProps={{ danger: true }}
okText={t('ok')}
onCancel={() => setPopOpen(false)}
onConfirm={(e) => {
e?.stopPropagation();
removeSession(id);
setPopOpen(false);
}}
open={popOpen}
overlayStyle={{ width: 280 }}
title={t('confirmRemoveSessionItemAlert')}
>
<Dropdown
arrow={false}
menu={{ items }}
onOpenChange={setDropdownOpen}
open={dropdownOpen}
trigger={['click']}
>
<ActionIcon
className="session-remove"
icon={MoreVertical}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
size={{
blockSize: 28,
fontSize: 16,
}}
/>
</Dropdown>
</Popconfirm>
<Dropdown arrow={false} menu={{ items }} trigger={['click']}>
<ActionIcon
className="session-remove"
icon={MoreVertical}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
size={{
blockSize: 28,
fontSize: 16,
}}
/>
</Dropdown>
</Flexbox>
</div>
);

View File

@@ -52,6 +52,9 @@ export const useStyles = createStyles(({ css, token }, isHighlight: boolean) =>
hover: css`
background-color: ${token.colorFillSecondary};
`,
modalRoot: css`
z-index: 2000;
`,
pin: css`
background-color: ${token.colorFillTertiary};
`,

View File

@@ -6,19 +6,32 @@ import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import HeaderTitle from '@/components/HeaderTitle';
import { exportSingleAgent, exportSingleSession } from '@/helpers/export';
import { useSessionStore } from '@/store/session';
const Header = memo(() => {
const { t } = useTranslation('setting');
const id = useSessionStore((s) => s.activeId);
const items: MenuProps['items'] = useMemo(
const items = useMemo<MenuProps['items']>(
() => [
{
key: 'agent',
label: <div>{t('exportType.agent', { ns: 'common' })}</div>,
onClick: () => {
if (!id) return;
exportSingleAgent(id);
},
},
{
key: 'agentWithMessage',
label: <div>{t('exportType.agentWithMessage', { ns: 'common' })}</div>,
onClick: () => {
if (!id) return;
exportSingleSession(id);
},
},
],
[],

View File

@@ -0,0 +1,23 @@
import { transform } from 'lodash-es';
import { SessionStore } from '@/store/session';
import { LobeAgentSession, LobeSessions } from '@/types/session';
import { getSessionById } from './list';
export const exportSessions = (s: SessionStore) => s.sessions;
// 排除 chats
export const exportAgents = (s: SessionStore) => {
return transform(s.sessions, (result: LobeSessions, value, key) => {
// 移除 chats 和 topics
result[key] = { ...value, chats: {}, topics: {} } as LobeAgentSession;
});
};
// 排除 chats
export const getExportAgent =
(id: string) =>
(s: SessionStore): LobeAgentSession => {
const session = getSessionById(id)(s);
return { ...session, chats: {}, topics: {} };
};

View File

@@ -1,14 +1,18 @@
import { exportAgents, exportSessions, getExportAgent } from './export';
import {
currentSession,
currentSessionSafe,
currentSessionSel,
getSessionById,
getSessionMetaById,
sessionList,
} from './list';
export const sessionSelectors = {
currentSession: currentSessionSel,
currentSession,
currentSessionSafe,
exportAgents,
exportSessions,
getExportAgent,
getSessionById,
getSessionMetaById,
sessionList,

View File

@@ -5,13 +5,14 @@ import { filterWithKeywords } from '@/utils/filter';
import { initLobeSession } from '../initialState';
export const currentSessionSel = (s: SessionStore): LobeAgentSession | undefined => {
export const currentSession = (s: SessionStore): LobeAgentSession | undefined => {
if (!s.activeId) return;
return s.sessions[s.activeId];
};
export const currentSessionSafe = (s: SessionStore): LobeAgentSession => {
return currentSessionSel(s) || initLobeSession;
return currentSession(s) || initLobeSession;
};
export const sessionList = (s: SessionStore) => {

View File

@@ -1,6 +1,7 @@
import { defaults } from 'lodash-es';
import { DEFAULT_SETTINGS } from '@/store/settings/initialState';
import { GlobalSettings } from '@/types/settings';
import { SettingsStore } from './store';
@@ -11,7 +12,16 @@ const selecThemeMode = (s: SettingsStore) => ({
themeMode: s.settings.themeMode,
});
export const exportSettings = (s: SettingsStore) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { OPENAI_API_KEY: _, password: __, ...settings } = s.settings;
return settings as GlobalSettings;
};
export const settingsSelectors = {
currentSettings,
exportSettings,
selecThemeMode,
};

View File

@@ -1,41 +1,61 @@
import { LobeSessions } from '@/types/session';
import { GlobalSettings } from '@/types/settings';
export interface ConfigState {
sessions: LobeSessions;
settings: GlobalSettings;
}
export interface SettingsConfigState {
settings: GlobalSettings;
}
export interface SessionsConfigState {
sessions: LobeSessions;
}
// 存在4种导出方式
export type ExportType = 'agents' | 'sessions' | 'settings' | 'all';
export type ConfigFile = ConfigFileSettings | ConfigFileSessions | ConfigFileAll | ConfigFileAgents;
// 4种方式对应的 state
export interface ConfigStateAll {
sessions: LobeSessions;
settings: GlobalSettings;
}
export interface ConfigStateSettings {
settings: GlobalSettings;
}
export interface ConfigStateSessions {
sessions: LobeSessions;
}
// 4种方式对应的 file
export interface ConfigFileSettings {
exportType: 'settings';
state: SettingsConfigState;
state: ConfigStateSettings;
version: number;
}
export interface ConfigFileSessions {
exportType: 'sessions';
state: SessionsConfigState;
state: ConfigStateSessions;
version: number;
}
export interface ConfigFileAgents {
exportType: 'agents';
state: SessionsConfigState;
state: ConfigStateSessions;
version: number;
}
export interface ConfigFileAll {
exportType: 'all';
state: ConfigStateAll;
version: number;
}
export interface ConfigFileAll {
exportType: 'all';
state: ConfigState;
version: number;
export type ConfigFile = ConfigFileSettings | ConfigFileSessions | ConfigFileAll | ConfigFileAgents;
// 用于 map 收集类型的 map
export interface ConfigModelMap {
agents: {
file: ConfigFileAgents;
state: ConfigStateSessions;
};
all: {
file: ConfigFileAll;
state: ConfigStateAll;
};
sessions: {
file: ConfigFileSessions;
state: ConfigStateSessions;
};
settings: {
file: ConfigFileSettings;
state: ConfigStateSettings;
};
}

View File

@@ -1,7 +1,15 @@
import { notification } from 'antd';
import { CURRENT_CONFIG_VERSION, Migration } from '@/migrations';
import { ConfigFile } from '@/types/exportConfig';
import {
ConfigFile,
ConfigFileAgents,
ConfigFileAll,
ConfigFileSessions,
ConfigFileSettings,
ConfigModelMap,
ExportType,
} from '@/types/exportConfig';
export const exportConfigFile = (config: object, fileName?: string) => {
const file = `LobeChat-${fileName || '-config'}-v${CURRENT_CONFIG_VERSION}.json`;
@@ -48,3 +56,48 @@ export const importConfigFile = (info: any, onConfigImport: (config: ConfigFile)
//@ts-ignore file 类型不明确
reader.readAsText(info.file.originFileObj, 'utf8');
};
type CreateConfigFileState<T extends ExportType> = ConfigModelMap[T]['state'];
type CreateConfigFile<T extends ExportType> = ConfigModelMap[T]['file'];
export const createConfigFile = <T extends ExportType>(
type: T,
state: CreateConfigFileState<T>,
): CreateConfigFile<T> => {
switch (type) {
case 'agents': {
return {
exportType: 'agents',
state,
version: Migration.targetVersion,
} as ConfigFileAgents;
}
case 'sessions': {
return {
exportType: 'sessions',
state,
version: Migration.targetVersion,
} as ConfigFileSessions;
}
case 'settings': {
return {
exportType: 'settings',
state,
version: Migration.targetVersion,
} as ConfigFileSettings;
}
case 'all': {
return {
exportType: 'all',
state,
version: Migration.targetVersion,
} as ConfigFileAll;
}
}
throw new Error('缺少正确的导出类型,请检查实现...');
};