@lobehub/chat 0.107.16 → 0.109.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 (98) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/docs/Environment-Variable.md +2 -2
  5. package/docs/Environment-Variable.zh-CN.md +2 -2
  6. package/locales/en_US/plugin.json +19 -0
  7. package/locales/en_US/setting.json +26 -0
  8. package/locales/ja_JP/plugin.json +19 -0
  9. package/locales/ja_JP/setting.json +26 -0
  10. package/locales/ko_KR/plugin.json +19 -0
  11. package/locales/ko_KR/setting.json +26 -0
  12. package/locales/ru_RU/plugin.json +19 -0
  13. package/locales/ru_RU/setting.json +26 -0
  14. package/locales/zh_CN/plugin.json +19 -0
  15. package/locales/zh_CN/setting.json +26 -5
  16. package/locales/zh_TW/plugin.json +19 -0
  17. package/locales/zh_TW/setting.json +26 -0
  18. package/package.json +3 -3
  19. package/src/app/chat/features/ChatHeader/PluginTag/PluginStatus.tsx +6 -6
  20. package/src/app/chat/features/ChatHeader/PluginTag/index.tsx +4 -4
  21. package/src/app/chat/features/ChatInput/ActionBar/Tools/ToolItem.tsx +37 -0
  22. package/src/app/chat/features/ChatInput/ActionBar/Tools/index.tsx +116 -0
  23. package/src/app/chat/features/ChatInput/ActionBar/config.ts +3 -1
  24. package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +4 -4
  25. package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/Settings.tsx +3 -3
  26. package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/index.tsx +4 -4
  27. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx +2 -2
  28. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +4 -4
  29. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/index.tsx +2 -2
  30. package/src/app/chat/features/Conversation/useInitConversation.ts +2 -2
  31. package/src/app/settings/(desktop)/index.tsx +3 -2
  32. package/src/app/settings/common/Common.tsx +8 -3
  33. package/src/app/settings/common/index.tsx +3 -3
  34. package/src/app/settings/common/page.tsx +7 -1
  35. package/src/app/settings/page.tsx +4 -1
  36. package/src/config/server.ts +5 -1
  37. package/src/features/AgentSetting/AgentPlugin/AddPluginButton.tsx +56 -0
  38. package/src/features/AgentSetting/AgentPlugin/LoadingList.tsx +48 -0
  39. package/src/features/AgentSetting/AgentPlugin/LocalPluginItem.tsx +5 -5
  40. package/src/features/AgentSetting/AgentPlugin/PluginAction/index.tsx +36 -0
  41. package/src/features/AgentSetting/AgentPlugin/index.tsx +157 -9
  42. package/src/features/AgentSetting/store/action.ts +0 -1
  43. package/src/features/PluginDevModal/MetaForm.tsx +3 -3
  44. package/src/features/PluginDevModal/PluginPreview.tsx +1 -1
  45. package/src/features/PluginDevModal/index.tsx +4 -1
  46. package/src/features/PluginSettings/index.tsx +3 -3
  47. package/src/features/PluginSettingsModal/index.tsx +22 -17
  48. package/src/features/PluginStore/Loading.tsx +13 -0
  49. package/src/features/PluginStore/PluginItem.tsx +81 -0
  50. package/src/features/PluginStore/PluginSettings.tsx +40 -0
  51. package/src/features/{AgentSetting/AgentPlugin/MarketSettingModal.tsx → PluginStore/StoreSetting.tsx} +2 -2
  52. package/src/features/PluginStore/index.tsx +95 -0
  53. package/src/layout/GlobalLayout/StoreHydration.tsx +3 -3
  54. package/src/locales/default/plugin.ts +19 -0
  55. package/src/locales/default/setting.ts +26 -5
  56. package/src/services/__tests__/chat.test.ts +221 -0
  57. package/src/services/__tests__/message.test.ts +333 -0
  58. package/src/services/__tests__/plugin.test.ts +88 -0
  59. package/src/services/__tests__/session.test.ts +227 -0
  60. package/src/services/__tests__/topic.test.ts +220 -0
  61. package/src/services/chat.ts +4 -6
  62. package/src/services/plugin.ts +12 -1
  63. package/src/store/chat/actions/message.test.ts +0 -1
  64. package/src/store/session/slices/agent/action.ts +24 -8
  65. package/src/store/{plugin → tool}/helpers.ts +2 -13
  66. package/src/store/tool/index.ts +2 -0
  67. package/src/store/tool/initialState.ts +11 -0
  68. package/src/store/tool/selectors.ts +3 -0
  69. package/src/store/tool/slices/customPlugin/action.test.ts +164 -0
  70. package/src/store/{plugin → tool}/slices/customPlugin/action.ts +2 -2
  71. package/src/store/tool/slices/customPlugin/selectors.test.ts +59 -0
  72. package/src/store/tool/slices/customPlugin/selectors.ts +9 -0
  73. package/src/store/tool/slices/plugin/action.test.ts +141 -0
  74. package/src/store/tool/slices/plugin/action.ts +95 -0
  75. package/src/store/{plugin → tool}/slices/plugin/initialState.ts +0 -7
  76. package/src/store/{plugin → tool/slices/plugin}/selectors.test.ts +4 -41
  77. package/src/store/{plugin → tool/slices/plugin}/selectors.ts +17 -19
  78. package/src/store/tool/slices/store/action.test.ts +396 -0
  79. package/src/store/tool/slices/store/action.ts +103 -0
  80. package/src/store/tool/slices/store/index.ts +3 -0
  81. package/src/store/tool/slices/store/initialState.ts +17 -0
  82. package/src/store/tool/slices/store/selectors.test.ts +48 -0
  83. package/src/store/tool/slices/store/selectors.ts +21 -0
  84. package/src/store/{plugin → tool}/store.ts +8 -6
  85. package/vitest.config.ts +1 -0
  86. package/src/features/AgentSetting/AgentPlugin/MarketList.tsx +0 -232
  87. package/src/features/AgentSetting/AgentPlugin/PluginSettings.tsx +0 -80
  88. package/src/store/chat/reducers/files.test.ts +0 -38
  89. package/src/store/chat/reducers/files.ts +0 -37
  90. package/src/store/plugin/hooks/useSessionHydrated.ts +0 -21
  91. package/src/store/plugin/index.ts +0 -2
  92. package/src/store/plugin/initialState.ts +0 -9
  93. package/src/store/plugin/slices/plugin/action.ts +0 -173
  94. /package/src/store/{plugin → tool}/slices/customPlugin/index.ts +0 -0
  95. /package/src/store/{plugin → tool}/slices/customPlugin/initialState.ts +0 -0
  96. /package/src/store/{plugin → tool}/slices/customPlugin/reducers/customPluginList.ts +0 -0
  97. /package/src/store/{plugin → tool}/slices/plugin/index.ts +0 -0
  98. /package/src/store/{plugin → tool}/slices/plugin/reducers/manifest.ts +0 -0
@@ -0,0 +1,164 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { CustomPlugin } from '@/types/plugin';
5
+
6
+ import { useToolStore } from '../../store';
7
+ import { defaultCustomPlugin } from './initialState';
8
+ import { CustomPluginListDispatch } from './reducers/customPluginList';
9
+
10
+ beforeEach(() => {
11
+ vi.resetAllMocks();
12
+ });
13
+
14
+ describe('useToolStore:customPlugin', () => {
15
+ describe('deleteCustomPlugin', () => {
16
+ it('should delete custom plugin and related settings', async () => {
17
+ // 设置初始状态和 mock 函数
18
+ const deletePluginSettingsMock = vi.fn();
19
+ act(() => {
20
+ useToolStore.setState({
21
+ // ...其他状态
22
+ customPluginList: [{ identifier: 'test-plugin' } as CustomPlugin],
23
+ deletePluginSettings: deletePluginSettingsMock,
24
+ });
25
+ });
26
+
27
+ const { result } = renderHook(() => useToolStore());
28
+ const pluginId = 'test-plugin';
29
+
30
+ act(() => {
31
+ result.current.deleteCustomPlugin(pluginId);
32
+ });
33
+
34
+ expect(deletePluginSettingsMock).toHaveBeenCalledWith(pluginId);
35
+ expect(result.current.customPluginList).toEqual([]);
36
+ });
37
+ });
38
+
39
+ describe('dispatchCustomPluginList', () => {
40
+ it('should update the custom plugin list based on the dispatched action', () => {
41
+ const initialState = {
42
+ customPluginList: [],
43
+ };
44
+ act(() => {
45
+ useToolStore.setState(initialState);
46
+ });
47
+
48
+ const payload = {
49
+ type: 'addItem',
50
+ plugin: { identifier: 'plugin1', meta: { title: 'Test Plugin' } },
51
+ } as CustomPluginListDispatch;
52
+
53
+ const { result } = renderHook(() => useToolStore());
54
+
55
+ act(() => {
56
+ result.current.dispatchCustomPluginList(payload);
57
+ });
58
+
59
+ expect(useToolStore.getState().customPluginList).toEqual([
60
+ { identifier: 'plugin1', meta: { title: 'Test Plugin' } },
61
+ ]);
62
+ });
63
+
64
+ it('should return same if the type is not in dispatch', () => {
65
+ const initialState = {
66
+ customPluginList: [],
67
+ };
68
+ act(() => {
69
+ useToolStore.setState(initialState);
70
+ });
71
+
72
+ const payload = {
73
+ type: 'adddItem',
74
+ plugin: { identifier: 'plugin1', meta: { title: 'Test Plugin' } },
75
+ } as unknown as CustomPluginListDispatch;
76
+
77
+ const { result } = renderHook(() => useToolStore());
78
+
79
+ act(() => {
80
+ result.current.dispatchCustomPluginList(payload);
81
+ });
82
+
83
+ expect(useToolStore.getState().customPluginList).toEqual([]);
84
+ });
85
+ });
86
+
87
+ describe('saveToCustomPluginList', () => {
88
+ it('should add a plugin to the custom plugin list and reset newCustomPlugin', async () => {
89
+ const newPlugin = { identifier: 'plugin2', meta: { title: 'New Plugin' } } as CustomPlugin;
90
+ act(() => {
91
+ useToolStore.setState({
92
+ customPluginList: [],
93
+ newCustomPlugin: newPlugin,
94
+ });
95
+ });
96
+
97
+ const { result } = renderHook(() => useToolStore());
98
+
99
+ act(() => {
100
+ result.current.saveToCustomPluginList(newPlugin);
101
+ });
102
+
103
+ expect(result.current.customPluginList).toEqual([newPlugin]);
104
+ expect(result.current.newCustomPlugin).toEqual(defaultCustomPlugin);
105
+ });
106
+ });
107
+ describe('updateCustomPlugin', () => {
108
+ it('should update a specific plugin in the custom plugin list and reinstall the plugin', () => {
109
+ const pluginId = 'test-plugin';
110
+ const old = {
111
+ identifier: pluginId,
112
+ meta: { title: 'Old Plugin', avatar: '🍎' },
113
+ } as CustomPlugin;
114
+
115
+ const installPluginMock = vi.fn();
116
+
117
+ act(() => {
118
+ useToolStore.setState({
119
+ customPluginList: [old],
120
+ installPlugin: installPluginMock,
121
+ });
122
+ });
123
+
124
+ const { result } = renderHook(() => useToolStore());
125
+
126
+ const updatedPlugin = {
127
+ identifier: pluginId,
128
+ meta: { title: 'Updated Plugin', avatar: '🥒' },
129
+ } as CustomPlugin;
130
+
131
+ act(() => {
132
+ result.current.updateCustomPlugin(pluginId, updatedPlugin);
133
+ });
134
+
135
+ expect(result.current.customPluginList).toEqual([updatedPlugin]);
136
+ expect(installPluginMock).toHaveBeenCalledWith(pluginId);
137
+ });
138
+ });
139
+
140
+ describe('updateNewCustomPlugin', () => {
141
+ it('should update the newCustomPlugin state with the provided values', () => {
142
+ const initialNewCustomPlugin = {
143
+ identifier: 'plugin3',
144
+ meta: { title: 'Initial Plugin' },
145
+ } as CustomPlugin;
146
+ const updates = { meta: { title: 'Updated Name' } } as Partial<CustomPlugin>;
147
+ const expectedNewCustomPlugin = { ...initialNewCustomPlugin, ...updates };
148
+
149
+ act(() => {
150
+ useToolStore.setState({
151
+ newCustomPlugin: initialNewCustomPlugin,
152
+ });
153
+ });
154
+
155
+ const { result } = renderHook(() => useToolStore());
156
+
157
+ act(() => {
158
+ result.current.updateNewCustomPlugin(updates);
159
+ });
160
+
161
+ expect(useToolStore.getState().newCustomPlugin).toEqual(expectedNewCustomPlugin);
162
+ });
163
+ });
164
+ });
@@ -4,7 +4,7 @@ import { StateCreator } from 'zustand/vanilla';
4
4
  import { CustomPlugin } from '@/types/plugin';
5
5
  import { setNamespace } from '@/utils/storeDebug';
6
6
 
7
- import { PluginStore } from '../../store';
7
+ import { ToolStore } from '../../store';
8
8
  import { defaultCustomPlugin } from './initialState';
9
9
  import { CustomPluginListDispatch, devPluginListReducer } from './reducers/customPluginList';
10
10
 
@@ -22,7 +22,7 @@ export interface CustomPluginAction {
22
22
  }
23
23
 
24
24
  export const createCustomPluginSlice: StateCreator<
25
- PluginStore,
25
+ ToolStore,
26
26
  [['zustand/devtools', never]],
27
27
  [],
28
28
  CustomPluginAction
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ToolStoreState } from '../../initialState';
4
+ import { customPluginSelectors } from './selectors';
5
+
6
+ const mockState = {
7
+ pluginManifestMap: {
8
+ 'plugin-1': {
9
+ identifier: 'plugin-1',
10
+ api: [{ name: 'api-1' }],
11
+ type: 'default',
12
+ },
13
+ 'plugin-2': {
14
+ identifier: 'plugin-2',
15
+ api: [{ name: 'api-2' }],
16
+ type: 'default',
17
+ },
18
+ },
19
+ pluginManifestLoading: {
20
+ 'plugin-1': false,
21
+ 'plugin-2': true,
22
+ },
23
+ pluginList: [
24
+ {
25
+ identifier: 'plugin-1',
26
+ author: 'Author 1',
27
+ createAt: '2021-01-01',
28
+ meta: { avatar: 'avatar-url-1', title: 'Plugin 1' },
29
+ homepage: 'http://homepage-1.com',
30
+ },
31
+ {
32
+ identifier: 'plugin-2',
33
+ author: 'Author 2',
34
+ createAt: '2022-02-02',
35
+ meta: { avatar: 'avatar-url-2', title: 'Plugin 2' },
36
+ homepage: 'http://homepage-2.com',
37
+ },
38
+ ],
39
+ customPluginList: [],
40
+ pluginsSettings: {},
41
+ } as unknown as ToolStoreState;
42
+
43
+ describe('pluginSelectors', () => {
44
+ describe('isCustomPlugin', () => {
45
+ it('should return false for a non-custom plugin', () => {
46
+ const result = customPluginSelectors.isCustomPlugin('plugin-1')(mockState);
47
+ expect(result).toBe(false);
48
+ });
49
+
50
+ it('should return true for a custom plugin', () => {
51
+ const stateWithCustomPlugin = {
52
+ ...mockState,
53
+ customPluginList: [{ identifier: 'custom-plugin' }],
54
+ } as ToolStoreState;
55
+ const result = customPluginSelectors.isCustomPlugin('custom-plugin')(stateWithCustomPlugin);
56
+ expect(result).toBe(true);
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,9 @@
1
+ import { pluginHelpers } from '../../helpers';
2
+ import type { ToolStoreState } from '../../initialState';
3
+
4
+ const isCustomPlugin = (id: string) => (s: ToolStoreState) =>
5
+ pluginHelpers.isCustomPlugin(id, s.customPluginList);
6
+
7
+ export const customPluginSelectors = {
8
+ isCustomPlugin,
9
+ };
@@ -0,0 +1,141 @@
1
+ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
2
+ import { act, renderHook, waitFor } from '@testing-library/react';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { LobeSessions } from '@/types/session';
6
+
7
+ import { useToolStore } from '../../store';
8
+ import { PluginDispatch } from './reducers/manifest';
9
+
10
+ beforeEach(() => {
11
+ // Reset all mocks before each test
12
+ vi.resetAllMocks();
13
+ });
14
+
15
+ describe('useToolStore:plugin', () => {
16
+ describe('checkLocalEnabledPlugins', () => {
17
+ it('should call checkPluginsIsInstalled with unique plugins from sessions', async () => {
18
+ // Mock sessions with some plugins
19
+ const sessions = [
20
+ { config: { plugins: ['plugin1', 'plugin2'] } },
21
+ { config: { plugins: ['plugin2', 'plugin3'] } },
22
+ ] as LobeSessions;
23
+
24
+ // Mock implementation for useToolStore
25
+ const checkPluginsIsInstalledMock = vi.fn();
26
+ useToolStore.setState({ checkPluginsIsInstalled: checkPluginsIsInstalledMock });
27
+
28
+ const { result } = renderHook(() => useToolStore());
29
+
30
+ // Call the action with our mock sessions
31
+ await act(async () => {
32
+ await result.current.checkLocalEnabledPlugins(sessions);
33
+ });
34
+
35
+ // Verify that checkPluginsIsInstalled was called with the correct unique plugins
36
+ expect(checkPluginsIsInstalledMock).toHaveBeenCalledWith(['plugin1', 'plugin2', 'plugin3']);
37
+ });
38
+
39
+ it('should not call checkPluginsIsInstalled if sessions have no plugins', async () => {
40
+ // Mock sessions with no plugins
41
+ const sessions = [
42
+ { config: { plugins: [] } },
43
+ { config: {} }, // config without plugins
44
+ ] as LobeSessions;
45
+
46
+ // Mock implementation for useToolStore
47
+ const checkPluginsIsInstalledMock = vi.fn();
48
+ useToolStore.setState({ checkPluginsIsInstalled: checkPluginsIsInstalledMock });
49
+
50
+ const { result } = renderHook(() => useToolStore());
51
+
52
+ // Call the action with our mock sessions
53
+ await act(async () => {
54
+ await result.current.checkLocalEnabledPlugins(sessions);
55
+ });
56
+
57
+ // Verify that checkPluginsIsInstalled was not called
58
+ expect(checkPluginsIsInstalledMock).not.toHaveBeenCalled();
59
+ });
60
+ });
61
+
62
+ describe('deletePluginSettings', () => {
63
+ it('should delete the settings for a given plugin', () => {
64
+ const pluginId = 'test-plugin';
65
+ useToolStore.setState({
66
+ pluginsSettings: {
67
+ [pluginId]: { setting1: 'value1' },
68
+ },
69
+ });
70
+
71
+ const { result } = renderHook(() => useToolStore());
72
+
73
+ act(() => {
74
+ result.current.deletePluginSettings(pluginId);
75
+ });
76
+
77
+ expect(useToolStore.getState().pluginsSettings[pluginId]).toBeUndefined();
78
+ });
79
+ });
80
+
81
+ describe('dispatchPluginManifest', () => {
82
+ it('should update the pluginManifestMap with the given payload', () => {
83
+ const payload: PluginDispatch = {
84
+ type: 'addManifest',
85
+ id: 'plugin1',
86
+ plugin: { identifier: 'plugin1' } as LobeChatPluginManifest,
87
+ };
88
+ useToolStore.setState({
89
+ pluginManifestMap: {},
90
+ });
91
+
92
+ const { result } = renderHook(() => useToolStore());
93
+
94
+ act(() => {
95
+ result.current.dispatchPluginManifest(payload);
96
+ });
97
+
98
+ expect(useToolStore.getState().pluginManifestMap).toHaveProperty(payload.id);
99
+ expect(useToolStore.getState().pluginManifestMap[payload.id]).toEqual(payload.plugin);
100
+ });
101
+ });
102
+
103
+ describe('updatePluginSettings', () => {
104
+ it('should update settings for a given plugin', () => {
105
+ const pluginId = 'test-plugin';
106
+ const newSettings = { setting1: 'new-value' };
107
+ useToolStore.setState({
108
+ pluginsSettings: {
109
+ [pluginId]: { setting1: 'value1' },
110
+ },
111
+ });
112
+
113
+ const { result } = renderHook(() => useToolStore());
114
+
115
+ act(() => {
116
+ result.current.updatePluginSettings(pluginId, newSettings);
117
+ });
118
+
119
+ expect(useToolStore.getState().pluginsSettings[pluginId]).toEqual(newSettings);
120
+ });
121
+ });
122
+
123
+ describe('resetPluginSettings', () => {
124
+ it('should reset all plugin settings', () => {
125
+ useToolStore.setState({
126
+ pluginsSettings: {
127
+ plugin1: { setting1: 'value1' },
128
+ plugin2: { setting2: 'value2' },
129
+ },
130
+ });
131
+
132
+ const { result } = renderHook(() => useToolStore());
133
+
134
+ act(() => {
135
+ result.current.resetPluginSettings();
136
+ });
137
+
138
+ expect(useToolStore.getState().pluginsSettings).toEqual({});
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,95 @@
1
+ import { produce } from 'immer';
2
+ import { merge, uniq } from 'lodash-es';
3
+ import useSWR, { SWRResponse } from 'swr';
4
+ import { StateCreator } from 'zustand/vanilla';
5
+
6
+ import { pluginStoreSelectors } from '@/store/tool/selectors';
7
+ import { LobeSessions } from '@/types/session';
8
+ import { setNamespace } from '@/utils/storeDebug';
9
+
10
+ import { ToolStore } from '../../store';
11
+ import { PluginDispatch, pluginManifestReducer } from './reducers/manifest';
12
+
13
+ const n = setNamespace('plugin');
14
+
15
+ /**
16
+ * 插件接口
17
+ */
18
+ export interface PluginAction {
19
+ checkLocalEnabledPlugins: (sessions: LobeSessions) => Promise<void>;
20
+ checkPluginsIsInstalled: (plugins: string[]) => Promise<void>;
21
+ deletePluginSettings: (id: string) => void;
22
+ dispatchPluginManifest: (payload: PluginDispatch) => void;
23
+ resetPluginSettings: () => void;
24
+ updatePluginSettings: <T>(id: string, settings: Partial<T>) => void;
25
+ useCheckPluginsIsInstalled: (plugins: string[]) => SWRResponse;
26
+ }
27
+
28
+ export const createPluginSlice: StateCreator<
29
+ ToolStore,
30
+ [['zustand/devtools', never]],
31
+ [],
32
+ PluginAction
33
+ > = (set, get) => ({
34
+ checkLocalEnabledPlugins: async (sessions) => {
35
+ const { checkPluginsIsInstalled } = get();
36
+
37
+ let enabledPlugins: string[] = [];
38
+
39
+ for (const session of sessions) {
40
+ const plugins = session.config.plugins;
41
+ if (!plugins || plugins.length === 0) continue;
42
+
43
+ enabledPlugins = [...enabledPlugins, ...plugins];
44
+ }
45
+
46
+ const plugins = uniq(enabledPlugins);
47
+
48
+ if (plugins.length === 0) return;
49
+ await checkPluginsIsInstalled(plugins);
50
+ },
51
+ checkPluginsIsInstalled: async (plugins) => {
52
+ // if there is no plugins, just skip.
53
+ if (plugins.length === 0) return;
54
+
55
+ const { loadPluginStore, installPlugins } = get();
56
+
57
+ // check if the store is empty
58
+ // if it is, we need to load the plugin store
59
+ if (pluginStoreSelectors.onlinePluginStore(get()).length === 0) {
60
+ await loadPluginStore();
61
+ }
62
+
63
+ await installPlugins(plugins);
64
+
65
+ set({ manifestPrepared: true }, false, n('checkPluginsIsInstalled'));
66
+ },
67
+ deletePluginSettings: (id) => {
68
+ set(
69
+ produce((draft) => {
70
+ draft.pluginsSettings[id] = undefined;
71
+ }),
72
+ false,
73
+ n('deletePluginSettings'),
74
+ );
75
+ },
76
+ dispatchPluginManifest: (payload) => {
77
+ const { pluginManifestMap } = get();
78
+ const nextManifest = pluginManifestReducer(pluginManifestMap, payload);
79
+
80
+ set({ pluginManifestMap: nextManifest }, false, n('dispatchPluginManifest', payload));
81
+ },
82
+ resetPluginSettings: () => {
83
+ set({ pluginsSettings: {} }, false, n('resetPluginSettings'));
84
+ },
85
+ updatePluginSettings: (id, settings) => {
86
+ set(
87
+ produce((draft) => {
88
+ draft.pluginsSettings[id] = merge({}, draft.pluginsSettings[id], settings);
89
+ }),
90
+ false,
91
+ n('updatePluginSettings'),
92
+ );
93
+ },
94
+ useCheckPluginsIsInstalled: (plugins) => useSWR(plugins, get().checkPluginsIsInstalled),
95
+ });
@@ -1,22 +1,15 @@
1
- import { LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
2
-
3
1
  import { PluginManifestMap } from '@/types/plugin';
4
2
 
5
- export type PluginManifestLoadingState = Record<string, boolean>;
6
3
  export type PluginsSettings = Record<string, any>;
7
4
 
8
5
  export interface PluginState {
9
6
  manifestPrepared: boolean;
10
- pluginList: LobeChatPluginMeta[];
11
- pluginManifestLoading: PluginManifestLoadingState;
12
7
  pluginManifestMap: PluginManifestMap;
13
8
  pluginsSettings: PluginsSettings;
14
9
  }
15
10
 
16
11
  export const initialPluginState: PluginState = {
17
12
  manifestPrepared: false,
18
- pluginList: [],
19
- pluginManifestLoading: {},
20
13
  pluginManifestMap: {},
21
14
  pluginsSettings: {},
22
15
  };
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { PluginStoreState } from './initialState';
3
+ import { ToolStoreState } from '../../initialState';
4
4
  import { pluginSelectors } from './selectors';
5
5
 
6
6
  const mockState = {
@@ -38,7 +38,7 @@ const mockState = {
38
38
  ],
39
39
  customPluginList: [],
40
40
  pluginsSettings: {},
41
- } as unknown as PluginStoreState;
41
+ } as unknown as ToolStoreState;
42
42
 
43
43
  describe('pluginSelectors', () => {
44
44
  describe('enabledSchema', () => {
@@ -58,27 +58,6 @@ describe('pluginSelectors', () => {
58
58
  expect(result).toEqual(mockState.pluginManifestMap['plugin-1']);
59
59
  });
60
60
  });
61
- describe('getPluginManifestLoadingStatus', () => {
62
- it('success', () => {
63
- const result = pluginSelectors.getPluginManifestLoadingStatus('plugin-1')(mockState);
64
- expect(result).toBe('success');
65
- });
66
- it('loading', () => {
67
- let result = pluginSelectors.getPluginManifestLoadingStatus('plugin-2')(mockState);
68
- expect(result).toBe('loading');
69
- });
70
- it('error', () => {
71
- let result = pluginSelectors.getPluginManifestLoadingStatus('plugin-3')(mockState);
72
- expect(result).toBe('error');
73
- });
74
- });
75
-
76
- describe('onlinePluginStore', () => {
77
- it('should return the online plugin list', () => {
78
- const result = pluginSelectors.onlinePluginStore(mockState);
79
- expect(result).toEqual(mockState.pluginList);
80
- });
81
- });
82
61
 
83
62
  describe('pluginList', () => {
84
63
  it('should return the combined list of pluginList and customPluginList', () => {
@@ -111,7 +90,7 @@ describe('pluginSelectors', () => {
111
90
  const stateWithCustomPlugin = {
112
91
  ...mockState,
113
92
  customPluginList: [customPlugin],
114
- } as PluginStoreState;
93
+ } as ToolStoreState;
115
94
  const result = pluginSelectors.getDevPluginById('custom-plugin')(stateWithCustomPlugin);
116
95
  expect(result).toEqual(customPlugin);
117
96
  });
@@ -129,22 +108,6 @@ describe('pluginSelectors', () => {
129
108
  });
130
109
  });
131
110
 
132
- describe('isCustomPlugin', () => {
133
- it('should return false for a non-custom plugin', () => {
134
- const result = pluginSelectors.isCustomPlugin('plugin-1')(mockState);
135
- expect(result).toBe(false);
136
- });
137
-
138
- it('should return true for a custom plugin', () => {
139
- const stateWithCustomPlugin = {
140
- ...mockState,
141
- customPluginList: [{ identifier: 'custom-plugin' }],
142
- } as PluginStoreState;
143
- const result = pluginSelectors.isCustomPlugin('custom-plugin')(stateWithCustomPlugin);
144
- expect(result).toBe(true);
145
- });
146
- });
147
-
148
111
  describe('displayPluginList', () => {
149
112
  it('should return a list of plugins with display information', () => {
150
113
  const result = pluginSelectors.displayPluginList(mockState);
@@ -177,7 +140,7 @@ describe('pluginSelectors', () => {
177
140
  ui: true,
178
141
  },
179
142
  },
180
- } as unknown as PluginStoreState;
143
+ } as unknown as ToolStoreState;
181
144
  const result = pluginSelectors.hasPluginUI('ui-plugin')(stateWithUIPlugin);
182
145
  expect(result).toBe(true);
183
146
  });
@@ -3,12 +3,12 @@ import { uniqBy } from 'lodash-es';
3
3
  import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
4
4
  import { ChatCompletionFunctions } from '@/types/openai/chat';
5
5
 
6
- import { pluginHelpers } from './helpers';
7
- import type { PluginStoreState } from './initialState';
6
+ import { pluginHelpers } from '../../helpers';
7
+ import type { ToolStoreState } from '../../initialState';
8
8
 
9
9
  const enabledSchema =
10
10
  (enabledPlugins: string[] = []) =>
11
- (s: PluginStoreState): ChatCompletionFunctions[] => {
11
+ (s: ToolStoreState): ChatCompletionFunctions[] => {
12
12
  // 如果不存在 enabledPlugins,那么全部不启用
13
13
  if (!enabledPlugins) return [];
14
14
 
@@ -34,34 +34,29 @@ const enabledSchema =
34
34
  return uniqBy(list, 'name');
35
35
  };
36
36
 
37
- const onlinePluginStore = (s: PluginStoreState) => s.pluginList;
37
+ const pluginList = (s: ToolStoreState) => [...s.pluginList, ...s.customPluginList];
38
38
 
39
- const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];
40
-
41
- const getPluginMetaById = (id: string) => (s: PluginStoreState) =>
39
+ const getPluginMetaById = (id: string) => (s: ToolStoreState) =>
42
40
  pluginHelpers.getPluginFormList(pluginList(s), id);
43
41
 
44
- const getDevPluginById = (id: string) => (s: PluginStoreState) =>
42
+ const getDevPluginById = (id: string) => (s: ToolStoreState) =>
45
43
  s.customPluginList.find((i) => i.identifier === id);
46
44
 
47
- const getPluginManifestById = (id: string) => (s: PluginStoreState) => s.pluginManifestMap[id];
48
- const getPluginSettingsById = (id: string) => (s: PluginStoreState) => s.pluginsSettings[id];
45
+ const getPluginManifestById = (id: string) => (s: ToolStoreState) => s.pluginManifestMap[id];
46
+ const getPluginSettingsById = (id: string) => (s: ToolStoreState) => s.pluginsSettings[id];
49
47
 
50
48
  // 获取插件 manifest 加载状态
51
- const getPluginManifestLoadingStatus = (id: string) => (s: PluginStoreState) => {
49
+ const getPluginManifestLoadingStatus = (id: string) => (s: ToolStoreState) => {
52
50
  const manifest = getPluginManifestById(id)(s);
53
51
 
54
- if (s.pluginManifestLoading[id]) return 'loading';
52
+ if (s.pluginInstallLoading[id]) return 'loading';
55
53
 
56
54
  if (!manifest) return 'error';
57
55
 
58
56
  if (!!manifest) return 'success';
59
57
  };
60
58
 
61
- const isCustomPlugin = (id: string) => (s: PluginStoreState) =>
62
- pluginHelpers.isCustomPlugin(id, s.customPluginList);
63
-
64
- const displayPluginList = (s: PluginStoreState) =>
59
+ const displayPluginList = (s: ToolStoreState) =>
65
60
  pluginList(s).map((p) => ({
66
61
  author: p.author,
67
62
  avatar: p.meta?.avatar,
@@ -72,12 +67,15 @@ const displayPluginList = (s: PluginStoreState) =>
72
67
  title: pluginHelpers.getPluginTitle(p.meta),
73
68
  }));
74
69
 
75
- const hasPluginUI = (id: string) => (s: PluginStoreState) => {
70
+ const hasPluginUI = (id: string) => (s: ToolStoreState) => {
76
71
  const manifest = getPluginManifestById(id)(s);
77
72
 
78
73
  return !!manifest?.ui;
79
74
  };
80
75
 
76
+ const installedPlugins = (s: ToolStoreState) => Object.values(s.pluginManifestMap);
77
+ const isPluginInstalled = (id: string) => (s: ToolStoreState) => !!s.pluginManifestMap[id];
78
+
81
79
  export const pluginSelectors = {
82
80
  displayPluginList,
83
81
  enabledSchema,
@@ -87,7 +85,7 @@ export const pluginSelectors = {
87
85
  getPluginMetaById,
88
86
  getPluginSettingsById,
89
87
  hasPluginUI,
90
- isCustomPlugin,
91
- onlinePluginStore,
88
+ installedPlugins,
89
+ isPluginInstalled,
92
90
  pluginList,
93
91
  };