@lobehub/chat 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.changelogrc.js +1 -0
  2. package/.commitlintrc.js +1 -0
  3. package/.editorconfig +16 -0
  4. package/.eslintignore +32 -0
  5. package/.eslintrc.js +6 -0
  6. package/.github/ISSUE_TEMPLATE/1_bug_report.yml +45 -0
  7. package/.github/ISSUE_TEMPLATE/2_feature_request.yml +21 -0
  8. package/.github/ISSUE_TEMPLATE/3_question.yml +15 -0
  9. package/.github/ISSUE_TEMPLATE/4_other.md +7 -0
  10. package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  11. package/.github/dependabot.yml +17 -0
  12. package/.github/workflows/auto-merge.yml +32 -0
  13. package/.github/workflows/contributor-help.yml +29 -0
  14. package/.github/workflows/issue-check-inactive.yml +22 -0
  15. package/.github/workflows/issue-close-require.yml +46 -0
  16. package/.github/workflows/issue-remove-inactive.yml +25 -0
  17. package/.github/workflows/release.yml +34 -0
  18. package/.github/workflows/test.yml +30 -0
  19. package/.gitpod.yml +3 -0
  20. package/.husky/commit-msg +4 -0
  21. package/.husky/pre-commit +5 -0
  22. package/.i18nrc.js +13 -0
  23. package/.prettierignore +63 -0
  24. package/.prettierrc.js +1 -0
  25. package/.releaserc.js +1 -0
  26. package/.remarkrc.js +1 -0
  27. package/.stylelintrc.js +8 -0
  28. package/CHANGELOG.md +80 -0
  29. package/README.md +147 -0
  30. package/locales/en_US/common.json +40 -0
  31. package/locales/en_US/setting.json +97 -0
  32. package/locales/zh_CN/common.json +40 -0
  33. package/locales/zh_CN/setting.json +98 -0
  34. package/next.config.mjs +32 -0
  35. package/package.json +138 -0
  36. package/public/next.svg +1 -0
  37. package/public/vercel.svg +1 -0
  38. package/scripts/genDefaultLocale.mjs +12 -0
  39. package/scripts/toc.mjs +40 -0
  40. package/src/const/fetch.ts +1 -0
  41. package/src/const/modelTokens.ts +8 -0
  42. package/src/features/FolderPanel/index.tsx +55 -0
  43. package/src/helpers/prompt.test.ts +36 -0
  44. package/src/helpers/prompt.ts +36 -0
  45. package/src/helpers/url.ts +17 -0
  46. package/src/layout/index.tsx +42 -0
  47. package/src/layout/style.ts +18 -0
  48. package/src/locales/create.ts +48 -0
  49. package/src/locales/default/common.ts +41 -0
  50. package/src/locales/default/setting.ts +97 -0
  51. package/src/locales/index.ts +5 -0
  52. package/src/locales/resources/en_US.ts +9 -0
  53. package/src/locales/resources/index.ts +7 -0
  54. package/src/locales/resources/zh_CN.ts +9 -0
  55. package/src/migrations/FromV0ToV1.ts +12 -0
  56. package/src/migrations/index.ts +13 -0
  57. package/src/pages/Sidebar.tsx +36 -0
  58. package/src/pages/_app.page.tsx +13 -0
  59. package/src/pages/_document.page.tsx +70 -0
  60. package/src/pages/api/LangChainStream.ts +95 -0
  61. package/src/pages/api/chain.api.ts +17 -0
  62. package/src/pages/api/openai.api.ts +31 -0
  63. package/src/pages/chat/SessionList/Header.tsx +56 -0
  64. package/src/pages/chat/SessionList/List/SessionItem.tsx +90 -0
  65. package/src/pages/chat/SessionList/List/index.tsx +31 -0
  66. package/src/pages/chat/SessionList/List/style.ts +77 -0
  67. package/src/pages/chat/SessionList/index.tsx +18 -0
  68. package/src/pages/chat/[id]/Config/ConfigCell.tsx +68 -0
  69. package/src/pages/chat/[id]/Config/ReadMode.tsx +63 -0
  70. package/src/pages/chat/[id]/Config/index.tsx +79 -0
  71. package/src/pages/chat/[id]/Conversation/ChatList.tsx +36 -0
  72. package/src/pages/chat/[id]/Conversation/Input.tsx +61 -0
  73. package/src/pages/chat/[id]/Conversation/index.tsx +32 -0
  74. package/src/pages/chat/[id]/Header.tsx +86 -0
  75. package/src/pages/chat/[id]/edit/AgentConfig.tsx +95 -0
  76. package/src/pages/chat/[id]/edit/AgentMeta.tsx +117 -0
  77. package/src/pages/chat/[id]/edit/FormItem.tsx +26 -0
  78. package/src/pages/chat/[id]/edit/Prompt.tsx +68 -0
  79. package/src/pages/chat/[id]/edit/index.page.tsx +62 -0
  80. package/src/pages/chat/[id]/edit/style.ts +42 -0
  81. package/src/pages/chat/[id]/index.page.tsx +40 -0
  82. package/src/pages/chat/index.page.tsx +1 -0
  83. package/src/pages/chat/layout.tsx +51 -0
  84. package/src/pages/index.page.tsx +1 -0
  85. package/src/pages/setting/Header.tsx +27 -0
  86. package/src/pages/setting/SettingForm.tsx +42 -0
  87. package/src/pages/setting/index.page.tsx +41 -0
  88. package/src/prompts/agent.ts +65 -0
  89. package/src/services/chatModel.ts +34 -0
  90. package/src/services/langChain.ts +18 -0
  91. package/src/services/url.ts +8 -0
  92. package/src/store/middleware/createHashStorage.ts +49 -0
  93. package/src/store/session/index.ts +33 -0
  94. package/src/store/session/initialState.ts +11 -0
  95. package/src/store/session/selectors.ts +3 -0
  96. package/src/store/session/slices/agentConfig/action.ts +226 -0
  97. package/src/store/session/slices/agentConfig/index.ts +3 -0
  98. package/src/store/session/slices/agentConfig/initialState.ts +34 -0
  99. package/src/store/session/slices/agentConfig/selectors.ts +54 -0
  100. package/src/store/session/slices/chat/action.ts +210 -0
  101. package/src/store/session/slices/chat/index.ts +3 -0
  102. package/src/store/session/slices/chat/initialState.ts +12 -0
  103. package/src/store/session/slices/chat/messageReducer.test.ts +70 -0
  104. package/src/store/session/slices/chat/messageReducer.ts +84 -0
  105. package/src/store/session/slices/chat/selectors.ts +83 -0
  106. package/src/store/session/slices/session/action.ts +118 -0
  107. package/src/store/session/slices/session/index.ts +3 -0
  108. package/src/store/session/slices/session/initialState.ts +31 -0
  109. package/src/store/session/slices/session/reducers/session.test.ts +456 -0
  110. package/src/store/session/slices/session/reducers/session.ts +113 -0
  111. package/src/store/session/slices/session/selectors/chat.ts +4 -0
  112. package/src/store/session/slices/session/selectors/index.ts +20 -0
  113. package/src/store/session/slices/session/selectors/list.ts +65 -0
  114. package/src/store/session/store.ts +17 -0
  115. package/src/store/settings/action.ts +31 -0
  116. package/src/store/settings/index.ts +23 -0
  117. package/src/store/settings/initialState.ts +25 -0
  118. package/src/store/settings/selectors.ts +9 -0
  119. package/src/store/settings/store.ts +13 -0
  120. package/src/styles/antdOverride.ts +29 -0
  121. package/src/styles/global.ts +23 -0
  122. package/src/styles/index.ts +6 -0
  123. package/src/types/chatMessage.ts +46 -0
  124. package/src/types/exportConfig.ts +23 -0
  125. package/src/types/global.d.ts +14 -0
  126. package/src/types/i18next.d.ts +8 -0
  127. package/src/types/langchain.ts +34 -0
  128. package/src/types/llm.ts +49 -0
  129. package/src/types/locale.ts +7 -0
  130. package/src/types/meta.ts +26 -0
  131. package/src/types/openai.ts +62 -0
  132. package/src/types/session.ts +59 -0
  133. package/src/utils/VersionController.test.ts +90 -0
  134. package/src/utils/VersionController.ts +64 -0
  135. package/src/utils/compass.ts +94 -0
  136. package/src/utils/fetch.ts +132 -0
  137. package/src/utils/filter.test.ts +120 -0
  138. package/src/utils/filter.ts +29 -0
  139. package/src/utils/uploadFIle.ts +8 -0
  140. package/src/utils/uuid.ts +9 -0
  141. package/tsconfig.json +26 -0
  142. package/vitest.config.ts +11 -0
@@ -0,0 +1,23 @@
1
+ import { create } from 'zustand';
2
+ import { type PersistOptions, devtools, persist } from 'zustand/middleware';
3
+
4
+ import { type SettingsStore, createStore } from './store';
5
+
6
+ export const LOBE_SETTINGS = 'LOBE_SETTINGS';
7
+
8
+ const persistOptions: PersistOptions<SettingsStore> = {
9
+ name: LOBE_SETTINGS,
10
+ skipHydration: true,
11
+ };
12
+
13
+ export const useSettings = create<SettingsStore>()(
14
+ persist(
15
+ devtools(createStore, {
16
+ name: LOBE_SETTINGS,
17
+ }),
18
+ persistOptions,
19
+ ),
20
+ );
21
+
22
+ export * from './selectors';
23
+ export type { SettingsStore } from './store';
@@ -0,0 +1,25 @@
1
+ import type { ThemeMode } from 'antd-style';
2
+
3
+ import type { ConfigSettings } from '@/types/exportConfig';
4
+
5
+ export type SidebarTabKey = 'chat' | 'market';
6
+ export const DEFAULT_SETTINGS: ConfigSettings = {
7
+ avatar: '',
8
+ };
9
+
10
+ export interface SettingsState {
11
+ inputHeight: number;
12
+ sessionExpandable?: boolean;
13
+ sessionsWidth: number;
14
+ settings: ConfigSettings;
15
+ sidebarKey: SidebarTabKey;
16
+ themeMode?: ThemeMode;
17
+ }
18
+
19
+ export const initialState: SettingsState = {
20
+ inputHeight: 200,
21
+ sessionExpandable: true,
22
+ sessionsWidth: 320,
23
+ settings: DEFAULT_SETTINGS,
24
+ sidebarKey: 'chat',
25
+ };
@@ -0,0 +1,9 @@
1
+ import { DEFAULT_SETTINGS } from '@/store/settings/initialState';
2
+
3
+ import { SettingsStore } from './store';
4
+
5
+ const currentSettings = (s: SettingsStore) => s.settings || DEFAULT_SETTINGS;
6
+
7
+ export const settingsSelectors = {
8
+ currentSettings,
9
+ };
@@ -0,0 +1,13 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { type SettingsAction, createSettings } from './action';
4
+ import { type SettingsState, initialState } from './initialState';
5
+
6
+ export type SettingsStore = SettingsAction & SettingsState;
7
+
8
+ export const createStore: StateCreator<SettingsStore, [['zustand/devtools', never]]> = (
9
+ ...parameters
10
+ ) => ({
11
+ ...initialState,
12
+ ...createSettings(...parameters),
13
+ });
@@ -0,0 +1,29 @@
1
+ import { Theme, css } from 'antd-style';
2
+ import { rgba } from 'polished';
3
+
4
+ export default (token: Theme) => css`
5
+ .ant-btn {
6
+ box-shadow: none;
7
+ }
8
+
9
+ .ant-popover {
10
+ z-index: 1100;
11
+ }
12
+
13
+ .ant-notification .ant-notification-notice.notification-primary-info {
14
+ background: ${token.colorPrimary};
15
+ box-shadow: 0 6px 16px 0 ${rgba(token.colorPrimary, 0.1)},
16
+ 0 3px 6px -4px ${rgba(token.colorPrimary, 0.2)},
17
+ 0 9px 28px 8px ${rgba(token.colorPrimary, 0.1)};
18
+
19
+ .anticon {
20
+ color: ${token.colorTextLightSolid};
21
+ }
22
+
23
+ .ant-notification-notice-message {
24
+ margin-bottom: 0;
25
+ padding-right: 0;
26
+ color: ${token.colorTextLightSolid};
27
+ }
28
+ }
29
+ `;
@@ -0,0 +1,23 @@
1
+ import { css } from 'antd-style';
2
+
3
+ export default () => css`
4
+ body,
5
+ .ant-app {
6
+ ::-webkit-scrollbar {
7
+ width: 0;
8
+ height: 0;
9
+ }
10
+ }
11
+
12
+ #__next {
13
+ height: 100%;
14
+ }
15
+
16
+ p {
17
+ margin-bottom: 0;
18
+ }
19
+
20
+ li {
21
+ display: block;
22
+ }
23
+ `;
@@ -0,0 +1,6 @@
1
+ import { createGlobalStyle } from 'antd-style';
2
+
3
+ import antdOverride from './antdOverride';
4
+ import global from './global';
5
+
6
+ export const GlobalStyle = createGlobalStyle(({ theme }) => [global(), antdOverride(theme)]);
@@ -0,0 +1,46 @@
1
+ import { LLMRoleType } from './llm';
2
+ import { BaseDataModel } from './meta';
3
+
4
+ /**
5
+ * 聊天消息错误对象
6
+ */
7
+ export interface ChatMessageError {
8
+ /**
9
+ * 错误信息
10
+ */
11
+ message: string;
12
+ status: number;
13
+ type: 'general' | 'llm';
14
+ }
15
+
16
+ export interface ChatMessage extends BaseDataModel {
17
+ archive?: boolean;
18
+
19
+ /**
20
+ * @title 内容
21
+ * @description 消息内容
22
+ */
23
+ content: string;
24
+ error?: any;
25
+
26
+ // 扩展字段
27
+ extra?: {
28
+ // 翻译
29
+ translate: {
30
+ target: string;
31
+ to: string;
32
+ };
33
+ // 语音
34
+ } & Record<string, any>;
35
+
36
+ parentId?: string;
37
+ // 引用
38
+ quotaId?: string;
39
+ /**
40
+ * 角色
41
+ * @description 消息发送者的角色
42
+ */
43
+ role: LLMRoleType;
44
+ }
45
+
46
+ export type ChatMessageMap = Record<string, ChatMessage>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 配置设置
3
+ */
4
+ export interface ConfigSettings {
5
+ /**
6
+ * 头像链接
7
+ */
8
+ avatar?: string;
9
+ }
10
+
11
+ export type ConfigKeys = keyof ConfigSettings;
12
+
13
+ export interface ConfigState {
14
+ settings: ConfigSettings;
15
+ }
16
+
17
+ export interface ConfigFile {
18
+ state: ConfigState;
19
+ /**
20
+ * 配置文件的版本号
21
+ */
22
+ version: number;
23
+ }
@@ -0,0 +1,14 @@
1
+ import type { LobeCustomStylish, LobeCustomToken } from '@lobehub/ui';
2
+ import 'antd-style';
3
+ import { AntdToken } from 'antd-style/lib/types/theme';
4
+
5
+ declare module 'antd-style' {
6
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
7
+ export interface CustomToken extends LobeCustomToken {}
8
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
9
+ export interface CustomStylish extends LobeCustomStylish {}
10
+ }
11
+
12
+ declare module 'styled-components' {
13
+ export interface DefaultTheme extends AntdToken, LobeCustomToken {}
14
+ }
@@ -0,0 +1,8 @@
1
+ import { DefaultResources } from '@/types/locale';
2
+
3
+ declare module 'i18next' {
4
+ interface CustomTypeOptions {
5
+ defaultNS: 'common';
6
+ resources: DefaultResources;
7
+ }
8
+ }
@@ -0,0 +1,34 @@
1
+ import { ChatMessage } from '@lobehub/ui';
2
+
3
+ export interface LangChainParams {
4
+ llm: {
5
+ /**
6
+ * 控制生成文本中的惩罚系数,用于减少重复性
7
+ */
8
+ frequency_penalty?: number;
9
+ /**
10
+ * 生成文本的最大长度
11
+ */
12
+ max_tokens?: number;
13
+ model: string;
14
+ /**
15
+ * 控制生成文本中的惩罚系数,用于减少主题的变化
16
+ */
17
+ presence_penalty?: number;
18
+ /**
19
+ * 生成文本的随机度量,用于控制文本的创造性和多样性
20
+ * @default 0.6
21
+ */
22
+ temperature: number;
23
+ /**
24
+ * 控制生成文本中最高概率的单个令牌
25
+ */
26
+ top_p?: number;
27
+ };
28
+
29
+ /**
30
+ * 聊天信息列表
31
+ */
32
+ prompts: ChatMessage[];
33
+ vars: Record<string, string>;
34
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * LLM 模型
3
+ */
4
+ export enum LanguageModel {
5
+ /**
6
+ * GPT 3.5 Turbo
7
+ */
8
+ GPT3_5 = 'gpt-3.5-turbo',
9
+ GPT3_5_16K = 'gpt-3.5-turbo-16k',
10
+ /**
11
+ * GPT 4
12
+ */
13
+ GPT4 = 'gpt-4',
14
+ GPT4_32K = 'gpt-4-32k',
15
+ }
16
+
17
+ // 语言模型的设置参数
18
+ export interface LLMParams {
19
+ /**
20
+ * 控制生成文本中的惩罚系数,用于减少重复性
21
+ */
22
+ frequency_penalty?: number;
23
+ /**
24
+ * 生成文本的最大长度
25
+ */
26
+ max_tokens?: number;
27
+ /**
28
+ * 控制生成文本中的惩罚系数,用于减少主题的变化
29
+ */
30
+ presence_penalty?: number;
31
+ /**
32
+ * 生成文本的随机度量,用于控制文本的创造性和多样性
33
+ * @default 0.6
34
+ */
35
+ temperature?: number;
36
+ /**
37
+ * 控制生成文本中最高概率的单个 token
38
+ */
39
+ top_p?: number;
40
+ }
41
+
42
+ export type LLMRoleType = 'user' | 'system' | 'assistant';
43
+
44
+ export interface LLMMessage {
45
+ content: string;
46
+ role: LLMRoleType;
47
+ }
48
+
49
+ export type LLMExample = LLMMessage[];
@@ -0,0 +1,7 @@
1
+ import resources from '@/locales/resources';
2
+ import defaultResources from '@/locales/resources/zh_CN';
3
+
4
+ export type Resources = typeof resources;
5
+ export type DefaultResources = typeof defaultResources;
6
+ export type Namespaces = keyof DefaultResources;
7
+ export type Locales = keyof Resources;
@@ -0,0 +1,26 @@
1
+ export interface MetaData {
2
+ /**
3
+ * 角色头像
4
+ * @description 可选参数,如果不传则使用默认头像
5
+ */
6
+ avatar?: string;
7
+ /**
8
+ * 背景色
9
+ * @description 可选参数,如果不传则使用默认背景色
10
+ */
11
+ backgroundColor?: string;
12
+ description?: string;
13
+ tag?: string[];
14
+ /**
15
+ * 名称
16
+ * @description 可选参数,如果不传则使用默认名称
17
+ */
18
+ title?: string;
19
+ }
20
+
21
+ export interface BaseDataModel {
22
+ createAt: number;
23
+ id: string;
24
+ meta: MetaData;
25
+ updateAt: number;
26
+ }
@@ -0,0 +1,62 @@
1
+ import { LLMRoleType } from '@/types/llm';
2
+
3
+ export interface OpenAIChatMessage {
4
+ /**
5
+ * @title 内容
6
+ * @description 消息内容
7
+ */
8
+ content: string;
9
+
10
+ /**
11
+ * 角色
12
+ * @description 消息发送者的角色
13
+ */
14
+ role: LLMRoleType;
15
+ }
16
+
17
+ /**
18
+ * @title OpenAI Stream Payload
19
+ */
20
+ export interface OpenAIStreamPayload {
21
+ /**
22
+ * @title 控制生成文本中的惩罚系数,用于减少重复性
23
+ * @default 0
24
+ */
25
+ frequency_penalty?: number;
26
+ /**
27
+ * @title 生成文本的最大长度
28
+ */
29
+ max_tokens?: number;
30
+ /**
31
+ * @title 聊天信息列表
32
+ */
33
+ messages: OpenAIChatMessage[];
34
+ /**
35
+ * @title 模型名称
36
+ */
37
+ model: string;
38
+ /**
39
+ * @title 返回的文本数量
40
+ */
41
+ n?: number;
42
+ /**
43
+ * @title 控制生成文本中的惩罚系数,用于减少主题的变化
44
+ * @default 0
45
+ */
46
+ presence_penalty?: number;
47
+ /**
48
+ * @title 是否开启流式请求
49
+ * @default true
50
+ */
51
+ stream?: boolean;
52
+ /**
53
+ * @title 生成文本的随机度量,用于控制文本的创造性和多样性
54
+ * @default 0.5
55
+ */
56
+ temperature: number;
57
+ /**
58
+ * @title 控制生成文本中最高概率的单个令牌
59
+ * @default 1
60
+ */
61
+ top_p?: number;
62
+ }
@@ -0,0 +1,59 @@
1
+ import { ChatMessageMap } from './chatMessage';
2
+ import { LLMExample, LLMParams, LanguageModel } from './llm';
3
+ import { BaseDataModel } from './meta';
4
+
5
+ export enum LobeSessionType {
6
+ /**
7
+ * 角色
8
+ */
9
+ Agent = 'agent',
10
+ /**
11
+ * 群聊
12
+ */
13
+ Group = 'group',
14
+ }
15
+
16
+ interface LobeSessionBase extends BaseDataModel {
17
+ /**
18
+ * 聊天记录
19
+ */
20
+ chats: ChatMessageMap;
21
+
22
+ /**
23
+ * 每个会话的类别
24
+ */
25
+ type: LobeSessionType;
26
+ }
27
+
28
+ export interface LobeAgentConfig {
29
+ /**
30
+ * 语言模型示例
31
+ */
32
+ example?: LLMExample;
33
+ /**
34
+ * 角色所使用的语言模型
35
+ * @default gpt-3.5-turbo
36
+ */
37
+ model: LanguageModel;
38
+ /**
39
+ * 语言模型参数
40
+ */
41
+ params: LLMParams;
42
+ /**
43
+ * 系统角色
44
+ */
45
+ systemRole: string;
46
+ }
47
+
48
+ /**
49
+ * Lobe Agent会话
50
+ */
51
+ export interface LobeAgentSession extends LobeSessionBase {
52
+ /**
53
+ * 语言模型角色设定
54
+ */
55
+ config: LobeAgentConfig;
56
+ type: LobeSessionType.Agent;
57
+ }
58
+
59
+ export type LobeSessions = Record<string, LobeAgentSession>;
@@ -0,0 +1,90 @@
1
+ import { Migration, MigrationData, VersionController } from './VersionController';
2
+
3
+ class TestMigration0 implements Migration {
4
+ version = 0;
5
+
6
+ migrate(data: MigrationData): MigrationData {
7
+ return data;
8
+ }
9
+ }
10
+ class TestMigration1 implements Migration {
11
+ version = 1;
12
+
13
+ migrate(data: MigrationData): MigrationData {
14
+ return {
15
+ state: {
16
+ ...data.state,
17
+ value1: data.state.value * 2,
18
+ },
19
+ version: this.version,
20
+ };
21
+ }
22
+ }
23
+
24
+ class TestMigration2 implements Migration {
25
+ version = 2;
26
+
27
+ migrate(data: MigrationData): MigrationData {
28
+ return {
29
+ state: {
30
+ ...data.state,
31
+ value2: data.state.value1 * 2,
32
+ },
33
+ version: this.version,
34
+ };
35
+ }
36
+ }
37
+
38
+ describe('VersionController', () => {
39
+ let migrations;
40
+ let versionController: VersionController<any>;
41
+
42
+ beforeEach(() => {
43
+ migrations = [TestMigration0, TestMigration1, TestMigration2];
44
+ versionController = new VersionController(migrations);
45
+ });
46
+
47
+ it('should instantiate with sorted migrations', () => {
48
+ expect(versionController['migrations'][0].version).toBe(0);
49
+ expect(versionController['migrations'][1].version).toBe(1);
50
+ expect(versionController['migrations'][2].version).toBe(2);
51
+ });
52
+
53
+ it('should throw error if data version is undefined', () => {
54
+ const data = {
55
+ state: { value: 10 },
56
+ };
57
+
58
+ expect(() => versionController.migrate(data as any)).toThrow(
59
+ '导入数据缺少版本号,请检查文件后重试',
60
+ );
61
+ });
62
+
63
+ it('should migrate data correctly through multiple versions', () => {
64
+ const data: MigrationData = {
65
+ state: { value: 10 },
66
+ version: 0,
67
+ };
68
+
69
+ const migratedData = versionController.migrate(data);
70
+
71
+ expect(migratedData).toEqual({
72
+ state: { value: 10, value1: 20, value2: 40 },
73
+ version: 3,
74
+ });
75
+ });
76
+
77
+ it('should migrate data correctly if starting from a specific version', () => {
78
+ const data: MigrationData = {
79
+ state: { value: 10, value1: 20 },
80
+ version: 1,
81
+ };
82
+
83
+ const migratedData = versionController.migrate(data);
84
+
85
+ expect(migratedData).toEqual({
86
+ state: { value: 10, value1: 20, value2: 40 },
87
+ version: 3,
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 迁移接口
3
+ * @template T - 状态类型
4
+ */
5
+ export interface Migration<T = any> {
6
+ /**
7
+ * 迁移数据
8
+ * @param data - 迁移数据
9
+ * @returns 迁移后的数据
10
+ */
11
+ migrate(data: MigrationData<T>): MigrationData;
12
+ /**
13
+ * 迁移版本号
14
+ */
15
+ version: number;
16
+ }
17
+
18
+ /**
19
+ * 迁移数据接口
20
+ * @template T - 状态类型
21
+ */
22
+ export interface MigrationData<T = any> {
23
+ /**
24
+ * 状态数据
25
+ */
26
+ state: T;
27
+ /**
28
+ * 迁移版本号
29
+ */
30
+ version: number;
31
+ }
32
+ export class VersionController<T> {
33
+ private migrations: Migration[];
34
+ targetVersion: number;
35
+
36
+ constructor(migrations: any[], targetVersion: number = migrations.length) {
37
+ this.migrations = migrations
38
+ .map((cls) => {
39
+ return new cls() as Migration;
40
+ })
41
+ .sort((a, b) => a.version - b.version);
42
+
43
+ this.targetVersion = targetVersion;
44
+ }
45
+
46
+ migrate(data: MigrationData<T>): MigrationData<T> {
47
+ let nextData = data;
48
+ const targetVersion = this.targetVersion || this.migrations.length;
49
+ if (data.version === undefined) throw new Error('导入数据缺少版本号,请检查文件后重试');
50
+ const currentVersion = data.version;
51
+
52
+ for (let i = currentVersion || 0; i < targetVersion; i++) {
53
+ const migration = this.migrations.find((m) => m.version === i);
54
+ if (!migration) throw new Error('程序出错');
55
+
56
+ nextData = migration.migrate(nextData);
57
+
58
+ nextData.version += 1;
59
+ console.log('迁移器:', migration, '数据:', nextData, '迁移后版本:', nextData.version);
60
+ }
61
+
62
+ return nextData;
63
+ }
64
+ }