@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,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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
7
|
-
import type {
|
|
6
|
+
import { pluginHelpers } from '../../helpers';
|
|
7
|
+
import type { ToolStoreState } from '../../initialState';
|
|
8
8
|
|
|
9
9
|
const enabledSchema =
|
|
10
10
|
(enabledPlugins: string[] = []) =>
|
|
11
|
-
(s:
|
|
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
|
|
37
|
+
const pluginList = (s: ToolStoreState) => [...s.pluginList, ...s.customPluginList];
|
|
38
38
|
|
|
39
|
-
const
|
|
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:
|
|
42
|
+
const getDevPluginById = (id: string) => (s: ToolStoreState) =>
|
|
45
43
|
s.customPluginList.find((i) => i.identifier === id);
|
|
46
44
|
|
|
47
|
-
const getPluginManifestById = (id: string) => (s:
|
|
48
|
-
const getPluginSettingsById = (id: string) => (s:
|
|
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:
|
|
49
|
+
const getPluginManifestLoadingStatus = (id: string) => (s: ToolStoreState) => {
|
|
52
50
|
const manifest = getPluginManifestById(id)(s);
|
|
53
51
|
|
|
54
|
-
if (s.
|
|
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
|
|
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:
|
|
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
|
-
|
|
91
|
-
|
|
88
|
+
installedPlugins,
|
|
89
|
+
isPluginInstalled,
|
|
92
90
|
pluginList,
|
|
93
91
|
};
|