@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,396 @@
1
+ import { LobeChatPluginManifest, LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { notification } from 'antd';
4
+ import useSWR from 'swr';
5
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import { pluginService } from '@/services/plugin';
8
+ import { CustomPlugin } from '@/types/plugin';
9
+
10
+ import { useToolStore } from '../../store';
11
+
12
+ // Mock the pluginService.getPluginList method
13
+ vi.mock('@/services/plugin', () => ({
14
+ pluginService: {
15
+ fetchManifest: vi.fn(),
16
+ getPluginList: vi.fn(),
17
+ },
18
+ }));
19
+
20
+ // Mock necessary modules and functions
21
+ vi.mock('antd', () => ({
22
+ notification: {
23
+ error: vi.fn(),
24
+ },
25
+ }));
26
+ // Mock i18next
27
+ vi.mock('i18next', () => ({
28
+ t: vi.fn((key) => key),
29
+ }));
30
+
31
+ const pluginManifestMock = {
32
+ $schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json',
33
+ api: [
34
+ {
35
+ url: 'https://realtime-weather.chat-plugin.lobehub.com/api/v1',
36
+ name: 'fetchCurrentWeather',
37
+ description: '获取当前天气情况',
38
+ parameters: {
39
+ properties: {
40
+ city: {
41
+ description: '城市名称',
42
+ type: 'string',
43
+ },
44
+ },
45
+ required: ['city'],
46
+ type: 'object',
47
+ },
48
+ },
49
+ ],
50
+ author: 'LobeHub',
51
+ createAt: '2023-08-12',
52
+ homepage: 'https://github.com/lobehub/chat-plugin-realtime-weather',
53
+ identifier: 'realtime-weather',
54
+ meta: {
55
+ avatar: '🌈',
56
+ tags: ['weather', 'realtime'],
57
+ title: 'Realtime Weather',
58
+ description: 'Get realtime weather information',
59
+ },
60
+ ui: {
61
+ url: 'https://realtime-weather.chat-plugin.lobehub.com/iframe',
62
+ height: 310,
63
+ },
64
+ version: '1',
65
+ };
66
+ // Mock useSWR
67
+ vi.mock('swr', () => ({
68
+ __esModule: true,
69
+ default: vi.fn(),
70
+ }));
71
+
72
+ const logError = console.error;
73
+ beforeEach(() => {
74
+ vi.restoreAllMocks();
75
+ useToolStore.setState({
76
+ pluginList: [
77
+ {
78
+ identifier: 'plugin1',
79
+ meta: { title: 'plugin1', avatar: '🍏' },
80
+ manifest: 'https://abc.com/manifest.json',
81
+ schemaVersion: 1,
82
+ } as LobeChatPluginMeta,
83
+ ],
84
+ });
85
+ console.error = () => {};
86
+ });
87
+ afterEach(() => {
88
+ console.error = logError;
89
+ });
90
+
91
+ describe('useToolStore:pluginStore', () => {
92
+ describe('loadPluginStore', () => {
93
+ it('should load plugin list and update state', async () => {
94
+ // Given
95
+ const pluginListMock = { plugins: [{ identifier: 'plugin1' }, { identifier: 'plugin2' }] };
96
+ (pluginService.getPluginList as Mock).mockResolvedValue(pluginListMock);
97
+
98
+ // When
99
+ let pluginList;
100
+ await act(async () => {
101
+ pluginList = await useToolStore.getState().loadPluginStore();
102
+ });
103
+
104
+ // Then
105
+ expect(pluginService.getPluginList).toHaveBeenCalled();
106
+ expect(pluginList).toEqual(pluginListMock);
107
+ expect(useToolStore.getState().pluginList).toEqual(pluginListMock.plugins);
108
+ });
109
+
110
+ it('should handle errors when loading plugin list', async () => {
111
+ // Given
112
+ const error = new Error('Failed to load plugin list');
113
+ (pluginService.getPluginList as Mock).mockRejectedValue(error);
114
+
115
+ // When
116
+ let pluginList;
117
+ let errorOccurred = false;
118
+ try {
119
+ await act(async () => {
120
+ pluginList = await useToolStore.getState().loadPluginStore();
121
+ });
122
+ } catch (e) {
123
+ errorOccurred = true;
124
+ }
125
+
126
+ // Then
127
+ expect(pluginService.getPluginList).toHaveBeenCalled();
128
+ expect(errorOccurred).toBe(true);
129
+ expect(pluginList).toBeUndefined();
130
+ // Ensure the state is not updated with an undefined value
131
+ expect(useToolStore.getState().pluginList).not.toBeUndefined();
132
+ });
133
+ });
134
+
135
+ describe('useFetchPluginStore', () => {
136
+ it('should use SWR to fetch plugin store', async () => {
137
+ // Given
138
+ const pluginListMock = { plugins: [{ identifier: 'plugin1' }, { identifier: 'plugin2' }] };
139
+ (useSWR as Mock).mockReturnValue({
140
+ data: pluginListMock,
141
+ error: null,
142
+ isValidating: false,
143
+ });
144
+
145
+ // When
146
+ const { result } = renderHook(() => useToolStore.getState().useFetchPluginStore());
147
+
148
+ // Then
149
+ expect(useSWR).toHaveBeenCalledWith('loadPluginStore', expect.any(Function));
150
+ expect(result.current.data).toEqual(pluginListMock);
151
+ expect(result.current.error).toBeNull();
152
+ expect(result.current.isValidating).toBe(false);
153
+ });
154
+
155
+ it('should handle errors when fetching plugin store with SWR', async () => {
156
+ // Given
157
+ const error = new Error('Failed to fetch plugin store');
158
+ (useSWR as Mock).mockReturnValue({
159
+ data: null,
160
+ error: error,
161
+ isValidating: false,
162
+ });
163
+
164
+ // When
165
+ const { result } = renderHook(() => useToolStore.getState().useFetchPluginStore());
166
+
167
+ // Then
168
+ expect(useSWR).toHaveBeenCalledWith('loadPluginStore', expect.any(Function));
169
+ expect(result.current.data).toBeNull();
170
+ expect(result.current.error).toEqual(error);
171
+ expect(result.current.isValidating).toBe(false);
172
+ });
173
+ });
174
+
175
+ describe('installPlugin', () => {
176
+ it('should install a plugin with valid manifest', async () => {
177
+ const pluginIdentifier = 'plugin1';
178
+
179
+ const originalUpdateInstallLoadingState = useToolStore.getState().updateInstallLoadingState;
180
+ const updateInstallLoadingStateMock = vi.fn();
181
+
182
+ act(() => {
183
+ useToolStore.setState({
184
+ updateInstallLoadingState: updateInstallLoadingStateMock,
185
+ });
186
+ });
187
+
188
+ const pluginManifestMock = {
189
+ $schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json',
190
+ api: [
191
+ {
192
+ url: 'https://realtime-weather.chat-plugin.lobehub.com/api/v1',
193
+ name: 'fetchCurrentWeather',
194
+ description: '获取当前天气情况',
195
+ parameters: {
196
+ properties: {
197
+ city: {
198
+ description: '城市名称',
199
+ type: 'string',
200
+ },
201
+ },
202
+ required: ['city'],
203
+ type: 'object',
204
+ },
205
+ },
206
+ ],
207
+ author: 'LobeHub',
208
+ createAt: '2023-08-12',
209
+ homepage: 'https://github.com/lobehub/chat-plugin-realtime-weather',
210
+ identifier: 'realtime-weather',
211
+ meta: {
212
+ avatar: '🌈',
213
+ tags: ['weather', 'realtime'],
214
+ title: 'Realtime Weather',
215
+ description: 'Get realtime weather information',
216
+ },
217
+ ui: {
218
+ url: 'https://realtime-weather.chat-plugin.lobehub.com/iframe',
219
+ height: 310,
220
+ },
221
+ version: '1',
222
+ };
223
+ (pluginService.fetchManifest as Mock).mockResolvedValue(pluginManifestMock);
224
+
225
+ await act(async () => {
226
+ await useToolStore.getState().installPlugin(pluginIdentifier);
227
+ });
228
+
229
+ // Then
230
+ expect(pluginService.fetchManifest).toHaveBeenCalled();
231
+ expect(notification.error).not.toHaveBeenCalled();
232
+ expect(updateInstallLoadingStateMock).toHaveBeenCalledTimes(2);
233
+ expect(useToolStore.getState().pluginManifestMap[pluginIdentifier]).toEqual(
234
+ pluginManifestMock,
235
+ );
236
+
237
+ act(() => {
238
+ useToolStore.setState({
239
+ updateInstallLoadingState: originalUpdateInstallLoadingState,
240
+ });
241
+ });
242
+ });
243
+
244
+ it('should throw error with no manifest url', async () => {
245
+ // Given
246
+
247
+ const pluginManifestMock = { identifier: 'plugin1', version: '1.0.0' };
248
+ (pluginService.fetchManifest as Mock).mockResolvedValue(pluginManifestMock);
249
+
250
+ useToolStore.setState({
251
+ pluginList: [
252
+ {
253
+ identifier: 'plugin1',
254
+ meta: { title: 'plugin1', avatar: '🍏' },
255
+ } as LobeChatPluginMeta,
256
+ ],
257
+ });
258
+
259
+ await act(async () => {
260
+ await useToolStore.getState().installPlugin('plugin1');
261
+ });
262
+
263
+ expect(notification.error).toHaveBeenCalledWith({
264
+ description: 'error.noManifest',
265
+ message: 'error.installError',
266
+ });
267
+ });
268
+
269
+ it('should throw error with invalid manifest', async () => {
270
+ const pluginManifestMock = { identifier: 'plugin1', version: '1.0.0' };
271
+ (pluginService.fetchManifest as Mock).mockResolvedValue(pluginManifestMock);
272
+
273
+ await act(async () => {
274
+ await useToolStore.getState().installPlugin('plugin1');
275
+ });
276
+
277
+ expect(pluginService.fetchManifest).toHaveBeenCalled();
278
+ expect(notification.error).toHaveBeenCalledWith({
279
+ description: 'error.manifestInvalid',
280
+ message: 'error.installError',
281
+ });
282
+ });
283
+
284
+ it('should handle error when fetch is error available', async () => {
285
+ (pluginService.fetchManifest as Mock).mockResolvedValue(undefined);
286
+
287
+ await act(async () => {
288
+ await useToolStore.getState().installPlugin('plugin1');
289
+ });
290
+
291
+ expect(pluginService.fetchManifest).toHaveBeenCalled();
292
+ expect(notification.error).toHaveBeenCalledWith({
293
+ description: 'error.fetchError',
294
+ message: 'error.installError',
295
+ });
296
+ });
297
+
298
+ // ... Add more test cases for error handling (fetchError and manifestInvalid)
299
+ });
300
+
301
+ describe('installPlugins', () => {
302
+ it('should install multiple plugins', async () => {
303
+ // Given
304
+ act(() => {
305
+ useToolStore.setState({
306
+ pluginList: [
307
+ {
308
+ identifier: 'plugin1',
309
+ meta: { title: 'plugin1', avatar: '🍏' },
310
+ manifest: 'https://abc.com/manifest.json',
311
+ } as LobeChatPluginMeta,
312
+ {
313
+ identifier: 'plugin2',
314
+ meta: { title: 'plugin2', avatar: '🍏' },
315
+ manifest: 'https://abc.com/manifest.json',
316
+ } as LobeChatPluginMeta,
317
+ ],
318
+ });
319
+ });
320
+
321
+ const plugins = ['plugin1', 'plugin2'];
322
+
323
+ (pluginService.fetchManifest as Mock).mockResolvedValue(pluginManifestMock);
324
+
325
+ // When
326
+ await act(async () => {
327
+ await useToolStore.getState().installPlugins(plugins);
328
+ });
329
+
330
+ // Then
331
+ expect(Object.keys(useToolStore.getState().pluginManifestMap)).toHaveLength(2);
332
+ });
333
+ });
334
+
335
+ describe('unInstallPlugin', () => {
336
+ it('should uninstall a plugin and remove its manifest', async () => {
337
+ // Given
338
+ const pluginIdentifier = 'plugin1';
339
+ act(() => {
340
+ useToolStore.setState({
341
+ pluginManifestMap: {
342
+ [pluginIdentifier]: {
343
+ identifier: pluginIdentifier,
344
+ meta: {},
345
+ } as LobeChatPluginManifest,
346
+ },
347
+ });
348
+ });
349
+
350
+ // When
351
+ act(() => {
352
+ useToolStore.getState().unInstallPlugin(pluginIdentifier);
353
+ });
354
+
355
+ // Then
356
+ expect(useToolStore.getState().pluginManifestMap[pluginIdentifier]).toBeUndefined();
357
+ });
358
+ });
359
+
360
+ describe('updateInstallLoadingState', () => {
361
+ it('should update the loading state for a plugin', () => {
362
+ const pluginIdentifier = 'plugin1';
363
+ const loadingState = true;
364
+ const { result } = renderHook(() => useToolStore());
365
+
366
+ act(() => {
367
+ result.current.updateInstallLoadingState(pluginIdentifier, loadingState);
368
+ });
369
+
370
+ expect(result.current.pluginInstallLoading[pluginIdentifier]).toBe(loadingState);
371
+ });
372
+
373
+ it('should clear the loading state for a plugin', () => {
374
+ // Given
375
+ const pluginIdentifier = 'plugin2';
376
+ const loadingState = undefined;
377
+
378
+ act(() => {
379
+ useToolStore.setState({
380
+ pluginInstallLoading: {
381
+ [pluginIdentifier]: true,
382
+ },
383
+ });
384
+ });
385
+ const { result } = renderHook(() => useToolStore());
386
+
387
+ // When
388
+ act(() => {
389
+ result.current.updateInstallLoadingState(pluginIdentifier, loadingState);
390
+ });
391
+
392
+ // Then
393
+ expect(result.current.pluginInstallLoading[pluginIdentifier]).toBe(loadingState);
394
+ });
395
+ });
396
+ });
@@ -0,0 +1,103 @@
1
+ import { LobeChatPluginsMarketIndex, pluginManifestSchema } from '@lobehub/chat-plugin-sdk';
2
+ import { notification } from 'antd';
3
+ import { t } from 'i18next';
4
+ import { produce } from 'immer';
5
+ import useSWR, { SWRResponse } from 'swr';
6
+ import { StateCreator } from 'zustand/vanilla';
7
+
8
+ import { pluginService } from '@/services/plugin';
9
+ import { pluginSelectors } from '@/store/tool/selectors';
10
+ import { setNamespace } from '@/utils/storeDebug';
11
+
12
+ import { ToolStore } from '../../store';
13
+ import { PluginStoreState } from './initialState';
14
+
15
+ const n = setNamespace('pluginStore');
16
+
17
+ interface PluginInstallError {
18
+ detail?: string;
19
+ message: 'noManifest' | 'fetchError' | 'manifestInvalid';
20
+ }
21
+
22
+ export interface PluginStoreAction {
23
+ installPlugin: (identifier: string) => Promise<void>;
24
+ installPlugins: (plugins: string[]) => Promise<void>;
25
+ loadPluginStore: () => Promise<LobeChatPluginsMarketIndex>;
26
+ unInstallPlugin: (identifier: string) => void;
27
+ updateInstallLoadingState: (key: string, value: boolean | undefined) => void;
28
+ useFetchPluginStore: () => SWRResponse<LobeChatPluginsMarketIndex>;
29
+ }
30
+
31
+ export const createPluginStoreSlice: StateCreator<
32
+ ToolStore,
33
+ [['zustand/devtools', never]],
34
+ [],
35
+ PluginStoreAction
36
+ > = (set, get) => ({
37
+ installPlugin: async (name) => {
38
+ const plugin = pluginSelectors.getPluginMetaById(name)(get());
39
+ if (!plugin) return;
40
+
41
+ try {
42
+ // 1. valid plugin
43
+
44
+ if (!plugin.manifest) {
45
+ throw new TypeError('noManifest');
46
+ }
47
+
48
+ // 2. 发送请求
49
+ get().updateInstallLoadingState(name, true);
50
+ const data = await pluginService.fetchManifest(plugin.manifest);
51
+
52
+ get().updateInstallLoadingState(name, undefined);
53
+ if (!data) {
54
+ throw new TypeError('fetchError');
55
+ }
56
+
57
+ // 3. 校验插件文件格式规范
58
+ const { success } = pluginManifestSchema.safeParse(data);
59
+
60
+ if (!success) {
61
+ throw new TypeError('manifestInvalid');
62
+ }
63
+
64
+ // 4. 存储 manifest 信息
65
+ get().dispatchPluginManifest({ id: plugin.identifier, plugin: data, type: 'addManifest' });
66
+ } catch (error) {
67
+ console.error(error);
68
+ const err = error as PluginInstallError;
69
+
70
+ notification.error({
71
+ description: t(`error.${err.message}`, { ns: 'plugin' }),
72
+ message: t('error.installError', { name: plugin.meta.title, ns: 'plugin' }),
73
+ });
74
+ }
75
+ },
76
+ installPlugins: async (plugins) => {
77
+ const { installPlugin } = get();
78
+
79
+ await Promise.all(plugins.map((identifier) => installPlugin(identifier)));
80
+ },
81
+ loadPluginStore: async () => {
82
+ const pluginMarketIndex = await pluginService.getPluginList();
83
+
84
+ set({ pluginList: pluginMarketIndex.plugins }, false, n('loadPluginList'));
85
+
86
+ return pluginMarketIndex;
87
+ },
88
+ unInstallPlugin: (identifier) => {
89
+ get().dispatchPluginManifest({ id: identifier, type: 'deleteManifest' });
90
+ },
91
+
92
+ updateInstallLoadingState: (key, value) => {
93
+ set(
94
+ produce((draft: PluginStoreState) => {
95
+ draft.pluginInstallLoading[key] = value;
96
+ }),
97
+ false,
98
+ n('updateInstallLoadingState'),
99
+ );
100
+ },
101
+ useFetchPluginStore: () =>
102
+ useSWR<LobeChatPluginsMarketIndex>('loadPluginStore', get().loadPluginStore),
103
+ });
@@ -0,0 +1,3 @@
1
+ export * from './action';
2
+ export * from './initialState';
3
+ // export * from './selectors';
@@ -0,0 +1,17 @@
1
+ import { LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
2
+
3
+ export type PluginInstallLoadingMap = Record<string, boolean | undefined>;
4
+
5
+ export interface PluginStoreState {
6
+ displayMode: 'grid' | 'list';
7
+ listType: 'all' | 'installed';
8
+ pluginInstallLoading: PluginInstallLoadingMap;
9
+ pluginList: LobeChatPluginMeta[];
10
+ }
11
+
12
+ export const initialPluginStoreState: PluginStoreState = {
13
+ displayMode: 'grid',
14
+ listType: 'all',
15
+ pluginInstallLoading: {},
16
+ pluginList: [],
17
+ };
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ToolStoreState } from '../../initialState';
4
+ import { pluginStoreSelectors } 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
+ } as unknown as ToolStoreState;
40
+
41
+ describe('pluginStoreSelectors', () => {
42
+ describe('onlinePluginStore', () => {
43
+ it('should return the online plugin list', () => {
44
+ const result = pluginStoreSelectors.onlinePluginStore(mockState);
45
+ expect(result).toEqual(mockState.pluginList);
46
+ });
47
+ });
48
+ });
@@ -0,0 +1,21 @@
1
+ import type { ToolStoreState } from '../../initialState';
2
+
3
+ const installedPlugins = (s: ToolStoreState) => Object.values(s.pluginManifestMap);
4
+
5
+ const onlinePluginStore = (s: ToolStoreState) => {
6
+ if (s.listType === 'all') return s.pluginList;
7
+
8
+ const installedPluginIds = Object.keys(s.pluginManifestMap);
9
+
10
+ return s.pluginList.filter((p) => installedPluginIds.includes(p.identifier));
11
+ };
12
+
13
+ const isPluginInstalled = (id: string) => (s: ToolStoreState) => !!s.pluginManifestMap[id];
14
+ const isPluginInstallLoading = (id: string) => (s: ToolStoreState) => s.pluginInstallLoading[id];
15
+
16
+ export const pluginStoreSelectors = {
17
+ installedPlugins,
18
+ isPluginInstallLoading,
19
+ isPluginInstalled,
20
+ onlinePluginStore,
21
+ };
@@ -5,28 +5,30 @@ import { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { isDev } from '@/utils/env';
7
7
 
8
- import { PluginStoreState, initialState } from './initialState';
8
+ import { ToolStoreState, initialState } from './initialState';
9
9
  import { CustomPluginAction, createCustomPluginSlice } from './slices/customPlugin';
10
10
  import { PluginAction, createPluginSlice } from './slices/plugin';
11
+ import { PluginStoreAction, createPluginStoreSlice } from './slices/store';
11
12
 
12
13
  // =============== 聚合 createStoreFn ============ //
13
14
 
14
- export type PluginStore = PluginStoreState & CustomPluginAction & PluginAction;
15
+ export type ToolStore = ToolStoreState & CustomPluginAction & PluginAction & PluginStoreAction;
15
16
 
16
- const createStore: StateCreator<PluginStore, [['zustand/devtools', never]]> = (...parameters) => ({
17
+ const createStore: StateCreator<ToolStore, [['zustand/devtools', never]]> = (...parameters) => ({
17
18
  ...initialState,
18
19
  ...createPluginSlice(...parameters),
19
20
  ...createCustomPluginSlice(...parameters),
21
+ ...createPluginStoreSlice(...parameters),
20
22
  });
21
23
 
22
24
  // =============== persist 本地缓存中间件配置 ============ //
23
25
 
24
26
  type SessionPersist = Pick<
25
- PluginStore,
27
+ ToolStore,
26
28
  'pluginList' | 'pluginManifestMap' | 'pluginsSettings' | 'customPluginList'
27
29
  >;
28
30
 
29
- const persistOptions: PersistOptions<PluginStore, SessionPersist> = {
31
+ const persistOptions: PersistOptions<ToolStore, SessionPersist> = {
30
32
  name: 'LOBE_PLUGIN',
31
33
 
32
34
  partialize: (s) => ({
@@ -43,7 +45,7 @@ const persistOptions: PersistOptions<PluginStore, SessionPersist> = {
43
45
 
44
46
  // =============== 实装 useStore ============ //
45
47
 
46
- export const usePluginStore = createWithEqualityFn<PluginStore>()(
48
+ export const useToolStore = createWithEqualityFn<ToolStore>()(
47
49
  persist(
48
50
  devtools(createStore, {
49
51
  name: 'LobeChat_Plugin' + (isDev ? '_DEV' : ''),
package/vitest.config.ts CHANGED
@@ -7,6 +7,7 @@ export default defineConfig({
7
7
  '@': resolve(__dirname, './src'),
8
8
  },
9
9
  coverage: {
10
+ all: false,
10
11
  exclude: ['__mocks__/**'],
11
12
  provider: 'v8',
12
13
  reporter: ['text', 'json', 'lcov', 'text-summary'],