@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.
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/docs/Environment-Variable.md +2 -2
- package/docs/Environment-Variable.zh-CN.md +2 -2
- package/locales/en_US/plugin.json +19 -0
- package/locales/en_US/setting.json +26 -0
- package/locales/ja_JP/plugin.json +19 -0
- package/locales/ja_JP/setting.json +26 -0
- package/locales/ko_KR/plugin.json +19 -0
- package/locales/ko_KR/setting.json +26 -0
- package/locales/ru_RU/plugin.json +19 -0
- package/locales/ru_RU/setting.json +26 -0
- package/locales/zh_CN/plugin.json +19 -0
- package/locales/zh_CN/setting.json +26 -5
- package/locales/zh_TW/plugin.json +19 -0
- package/locales/zh_TW/setting.json +26 -0
- package/package.json +3 -3
- package/src/app/chat/features/ChatHeader/PluginTag/PluginStatus.tsx +6 -6
- package/src/app/chat/features/ChatHeader/PluginTag/index.tsx +4 -4
- package/src/app/chat/features/ChatInput/ActionBar/Tools/ToolItem.tsx +37 -0
- package/src/app/chat/features/ChatInput/ActionBar/Tools/index.tsx +116 -0
- package/src/app/chat/features/ChatInput/ActionBar/config.ts +3 -1
- package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +4 -4
- package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/Settings.tsx +3 -3
- package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/index.tsx +4 -4
- package/src/app/chat/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +4 -4
- package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/index.tsx +2 -2
- package/src/app/chat/features/Conversation/useInitConversation.ts +2 -2
- package/src/app/settings/(desktop)/index.tsx +3 -2
- package/src/app/settings/common/Common.tsx +8 -3
- package/src/app/settings/common/index.tsx +3 -3
- package/src/app/settings/common/page.tsx +7 -1
- package/src/app/settings/page.tsx +4 -1
- package/src/config/server.ts +5 -1
- package/src/features/AgentSetting/AgentPlugin/AddPluginButton.tsx +56 -0
- package/src/features/AgentSetting/AgentPlugin/LoadingList.tsx +48 -0
- package/src/features/AgentSetting/AgentPlugin/LocalPluginItem.tsx +5 -5
- package/src/features/AgentSetting/AgentPlugin/PluginAction/index.tsx +36 -0
- package/src/features/AgentSetting/AgentPlugin/index.tsx +157 -9
- package/src/features/AgentSetting/store/action.ts +0 -1
- package/src/features/PluginDevModal/MetaForm.tsx +3 -3
- package/src/features/PluginDevModal/PluginPreview.tsx +1 -1
- package/src/features/PluginDevModal/index.tsx +4 -1
- package/src/features/PluginSettings/index.tsx +3 -3
- package/src/features/PluginSettingsModal/index.tsx +22 -17
- package/src/features/PluginStore/Loading.tsx +13 -0
- package/src/features/PluginStore/PluginItem.tsx +81 -0
- package/src/features/PluginStore/PluginSettings.tsx +40 -0
- package/src/features/{AgentSetting/AgentPlugin/MarketSettingModal.tsx → PluginStore/StoreSetting.tsx} +2 -2
- package/src/features/PluginStore/index.tsx +95 -0
- package/src/layout/GlobalLayout/StoreHydration.tsx +3 -3
- package/src/locales/default/plugin.ts +19 -0
- package/src/locales/default/setting.ts +26 -5
- package/src/services/__tests__/chat.test.ts +221 -0
- package/src/services/__tests__/message.test.ts +333 -0
- package/src/services/__tests__/plugin.test.ts +88 -0
- package/src/services/__tests__/session.test.ts +227 -0
- package/src/services/__tests__/topic.test.ts +220 -0
- package/src/services/chat.ts +4 -6
- package/src/services/plugin.ts +12 -1
- package/src/store/chat/actions/message.test.ts +0 -1
- package/src/store/session/slices/agent/action.ts +24 -8
- package/src/store/{plugin → tool}/helpers.ts +2 -13
- package/src/store/tool/index.ts +2 -0
- package/src/store/tool/initialState.ts +11 -0
- package/src/store/tool/selectors.ts +3 -0
- package/src/store/tool/slices/customPlugin/action.test.ts +164 -0
- package/src/store/{plugin → tool}/slices/customPlugin/action.ts +2 -2
- package/src/store/tool/slices/customPlugin/selectors.test.ts +59 -0
- package/src/store/tool/slices/customPlugin/selectors.ts +9 -0
- package/src/store/tool/slices/plugin/action.test.ts +141 -0
- package/src/store/tool/slices/plugin/action.ts +95 -0
- package/src/store/{plugin → tool}/slices/plugin/initialState.ts +0 -7
- package/src/store/{plugin → tool/slices/plugin}/selectors.test.ts +4 -41
- package/src/store/{plugin → tool/slices/plugin}/selectors.ts +17 -19
- package/src/store/tool/slices/store/action.test.ts +396 -0
- package/src/store/tool/slices/store/action.ts +103 -0
- package/src/store/tool/slices/store/index.ts +3 -0
- package/src/store/tool/slices/store/initialState.ts +17 -0
- package/src/store/tool/slices/store/selectors.test.ts +48 -0
- package/src/store/tool/slices/store/selectors.ts +21 -0
- package/src/store/{plugin → tool}/store.ts +8 -6
- package/vitest.config.ts +1 -0
- package/src/features/AgentSetting/AgentPlugin/MarketList.tsx +0 -232
- package/src/features/AgentSetting/AgentPlugin/PluginSettings.tsx +0 -80
- package/src/store/chat/reducers/files.test.ts +0 -38
- package/src/store/chat/reducers/files.ts +0 -37
- package/src/store/plugin/hooks/useSessionHydrated.ts +0 -21
- package/src/store/plugin/index.ts +0 -2
- package/src/store/plugin/initialState.ts +0 -9
- package/src/store/plugin/slices/plugin/action.ts +0 -173
- /package/src/store/{plugin → tool}/slices/customPlugin/index.ts +0 -0
- /package/src/store/{plugin → tool}/slices/customPlugin/initialState.ts +0 -0
- /package/src/store/{plugin → tool}/slices/customPlugin/reducers/customPluginList.ts +0 -0
- /package/src/store/{plugin → tool}/slices/plugin/index.ts +0 -0
- /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,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 {
|
|
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
|
|
15
|
+
export type ToolStore = ToolStoreState & CustomPluginAction & PluginAction & PluginStoreAction;
|
|
15
16
|
|
|
16
|
-
const createStore: StateCreator<
|
|
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
|
-
|
|
27
|
+
ToolStore,
|
|
26
28
|
'pluginList' | 'pluginManifestMap' | 'pluginsSettings' | 'customPluginList'
|
|
27
29
|
>;
|
|
28
30
|
|
|
29
|
-
const persistOptions: PersistOptions<
|
|
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
|
|
48
|
+
export const useToolStore = createWithEqualityFn<ToolStore>()(
|
|
47
49
|
persist(
|
|
48
50
|
devtools(createStore, {
|
|
49
51
|
name: 'LobeChat_Plugin' + (isDev ? '_DEV' : ''),
|