@lobehub/chat 0.120.0 → 0.120.2
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 +24 -26
- package/README.zh-CN.md +24 -26
- package/docs/Deployment/Common-Error.md +18 -1
- package/docs/Deployment/Common-Error.zh-CN.md +18 -0
- package/docs/Deployment/Environment-Variable.md +14 -3
- package/docs/Deployment/Environment-Variable.zh-CN.md +14 -3
- package/docs/Development/Contributing-Guidelines.zh-CN.md +72 -0
- package/docs/Development/Internationalization-Implementation.zh-CN.md +117 -0
- package/docs/Development/State-Management/Selectors.zh-CN.md +49 -0
- package/docs/Development/State-Management/index.zh-CN.md +219 -0
- package/docs/Development/index.zh-CN.md +66 -1
- package/docs/Home.md +3 -3
- package/docs/_Sidebar.md +4 -4
- package/package.json +7 -3
- package/src/features/Conversation/Plugins/Render/StandaloneType/Iframe.tsx +2 -2
- package/src/features/Conversation/Plugins/Render/utils/listenToPlugin.test.ts +2 -2
- package/src/features/Conversation/Plugins/Render/utils/listenToPlugin.ts +5 -2
- package/src/features/Conversation/components/ChatItem/index.tsx +12 -1
- package/src/features/Conversation/components/VirtualizedList/index.tsx +4 -1
- package/src/store/chat/slices/tool/action.test.ts +167 -7
- package/src/store/chat/slices/tool/action.ts +36 -4
- package/src/store/tool/slices/plugin/__snapshots__/action.test.ts.snap +15 -0
- package/src/store/tool/slices/plugin/action.test.ts +122 -0
- package/src/store/tool/slices/plugin/action.ts +18 -1
- package/docs/Development/Data-Store-Selectors.zh-CN.md +0 -68
- /package/docs/{Add-New-Locale.md → Development/Add-New-Locale.md} +0 -0
- /package/docs/{Add-New-Locale.zh-CN.md → Development/Add-New-Locale.zh-CN.md} +0 -0
- /package/docs/Development/{Data-Store-Selectors.md → State-Management/Selectors.md} +0 -0
|
@@ -12,6 +12,8 @@ import { pluginSelectors } from '@/store/tool/selectors';
|
|
|
12
12
|
import { ChatPluginPayload } from '@/types/message';
|
|
13
13
|
import { LobeTool } from '@/types/tool';
|
|
14
14
|
|
|
15
|
+
const invokeStandaloneTypePlugin = useChatStore.getState().invokeStandaloneTypePlugin;
|
|
16
|
+
|
|
15
17
|
// Mock messageService 和 chatSelectors
|
|
16
18
|
vi.mock('@/services/message', () => ({
|
|
17
19
|
messageService: {
|
|
@@ -41,7 +43,7 @@ beforeEach(() => {
|
|
|
41
43
|
|
|
42
44
|
describe('ChatPluginAction', () => {
|
|
43
45
|
describe('fillPluginMessageContent', () => {
|
|
44
|
-
it('should update message content and
|
|
46
|
+
it('should update message content and trigger the ai message', async () => {
|
|
45
47
|
// 设置模拟函数的返回值
|
|
46
48
|
const mockCurrentChats: any[] = [];
|
|
47
49
|
(chatSelectors.currentChats as Mock).mockReturnValue(mockCurrentChats);
|
|
@@ -60,7 +62,7 @@ describe('ChatPluginAction', () => {
|
|
|
60
62
|
const newContent = 'Updated content';
|
|
61
63
|
|
|
62
64
|
await act(async () => {
|
|
63
|
-
await result.current.fillPluginMessageContent(messageId, newContent);
|
|
65
|
+
await result.current.fillPluginMessageContent(messageId, newContent, true);
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
// 验证 messageService.updateMessageContent 是否被正确调用
|
|
@@ -72,6 +74,37 @@ describe('ChatPluginAction', () => {
|
|
|
72
74
|
// 验证 coreProcessMessage 是否被正确调用
|
|
73
75
|
expect(result.current.coreProcessMessage).toHaveBeenCalledWith(mockCurrentChats, messageId);
|
|
74
76
|
});
|
|
77
|
+
it('should update message content and not trigger ai message', async () => {
|
|
78
|
+
// 设置模拟函数的返回值
|
|
79
|
+
const mockCurrentChats: any[] = [];
|
|
80
|
+
(chatSelectors.currentChats as Mock).mockReturnValue(mockCurrentChats);
|
|
81
|
+
|
|
82
|
+
// 设置初始状态
|
|
83
|
+
const initialState = {
|
|
84
|
+
messages: [],
|
|
85
|
+
coreProcessMessage: vi.fn(),
|
|
86
|
+
refreshMessages: vi.fn(),
|
|
87
|
+
};
|
|
88
|
+
useChatStore.setState(initialState);
|
|
89
|
+
|
|
90
|
+
const { result } = renderHook(() => useChatStore());
|
|
91
|
+
|
|
92
|
+
const messageId = 'message-id';
|
|
93
|
+
const newContent = 'Updated content';
|
|
94
|
+
|
|
95
|
+
await act(async () => {
|
|
96
|
+
await result.current.fillPluginMessageContent(messageId, newContent);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// 验证 messageService.updateMessageContent 是否被正确调用
|
|
100
|
+
expect(messageService.updateMessage).toHaveBeenCalledWith(messageId, { content: newContent });
|
|
101
|
+
|
|
102
|
+
// 验证 refreshMessages 是否被调用
|
|
103
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
104
|
+
|
|
105
|
+
// 验证 coreProcessMessage 没有被正确调用
|
|
106
|
+
expect(result.current.coreProcessMessage).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
75
108
|
});
|
|
76
109
|
|
|
77
110
|
describe('runPluginDefaultType', () => {
|
|
@@ -259,9 +292,92 @@ describe('ChatPluginAction', () => {
|
|
|
259
292
|
],
|
|
260
293
|
});
|
|
261
294
|
|
|
295
|
+
const invokeStandaloneTypePlugin = useChatStore.getState().invokeStandaloneTypePlugin;
|
|
296
|
+
|
|
297
|
+
act(() => {
|
|
298
|
+
useChatStore.setState({
|
|
299
|
+
refreshMessages: vi.fn(),
|
|
300
|
+
invokeStandaloneTypePlugin: vi.fn(),
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
(chatSelectors.getMessageById as Mock).mockImplementation(() => () => ({
|
|
305
|
+
id: messageId,
|
|
306
|
+
content: messageContent,
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
const { result } = renderHook(() => useChatStore());
|
|
310
|
+
|
|
311
|
+
await act(async () => {
|
|
312
|
+
await result.current.triggerFunctionCall(messageId);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// 验证 refreshMessages 是否被调用
|
|
316
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
317
|
+
|
|
318
|
+
// 验证 invokeDefaultTypePlugin 是否没有被调用,因为类型是 standalone
|
|
319
|
+
expect(result.current.invokeDefaultTypePlugin).not.toHaveBeenCalled();
|
|
320
|
+
expect(result.current.invokeStandaloneTypePlugin).toHaveBeenCalled();
|
|
321
|
+
|
|
322
|
+
useChatStore.setState({ invokeStandaloneTypePlugin });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should handle builtin plugin type', async () => {
|
|
326
|
+
const messageId = 'message-id';
|
|
327
|
+
const messageContent = JSON.stringify({
|
|
328
|
+
tool_calls: [
|
|
329
|
+
{
|
|
330
|
+
id: 'call_scv',
|
|
331
|
+
function: {
|
|
332
|
+
name: `pluginName${PLUGIN_SCHEMA_SEPARATOR}apiName${PLUGIN_SCHEMA_SEPARATOR}builtin`,
|
|
333
|
+
arguments: {},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const invokeBuiltinTool = useChatStore.getState().invokeBuiltinTool;
|
|
340
|
+
useChatStore.setState({ refreshMessages: vi.fn(), invokeBuiltinTool: vi.fn() });
|
|
341
|
+
|
|
342
|
+
(chatSelectors.getMessageById as Mock).mockImplementation(() => () => ({
|
|
343
|
+
id: messageId,
|
|
344
|
+
content: messageContent,
|
|
345
|
+
}));
|
|
346
|
+
|
|
347
|
+
const { result } = renderHook(() => useChatStore());
|
|
348
|
+
|
|
349
|
+
await act(async () => {
|
|
350
|
+
await result.current.triggerFunctionCall(messageId);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// 验证 refreshMessages 是否被调用
|
|
354
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
355
|
+
|
|
356
|
+
// 验证 invokeDefaultTypePlugin 是否没有被调用,因为类型是 standalone
|
|
357
|
+
expect(result.current.invokeDefaultTypePlugin).not.toHaveBeenCalled();
|
|
358
|
+
expect(result.current.invokeBuiltinTool).toHaveBeenCalled();
|
|
359
|
+
|
|
360
|
+
useChatStore.setState({ invokeBuiltinTool });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should handle markdown plugin type', async () => {
|
|
364
|
+
const messageId = 'message-id';
|
|
365
|
+
const messageContent = JSON.stringify({
|
|
366
|
+
tool_calls: [
|
|
367
|
+
{
|
|
368
|
+
id: 'call_scv',
|
|
369
|
+
function: {
|
|
370
|
+
name: `pluginName${PLUGIN_SCHEMA_SEPARATOR}apiName${PLUGIN_SCHEMA_SEPARATOR}markdown`,
|
|
371
|
+
arguments: {},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const invokeMarkdownTypePlugin = useChatStore.getState().invokeMarkdownTypePlugin;
|
|
262
378
|
useChatStore.setState({
|
|
263
379
|
refreshMessages: vi.fn(),
|
|
264
|
-
|
|
380
|
+
invokeMarkdownTypePlugin: vi.fn(),
|
|
265
381
|
});
|
|
266
382
|
|
|
267
383
|
(chatSelectors.getMessageById as Mock).mockImplementation(() => () => ({
|
|
@@ -278,8 +394,10 @@ describe('ChatPluginAction', () => {
|
|
|
278
394
|
// 验证 refreshMessages 是否被调用
|
|
279
395
|
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
280
396
|
|
|
281
|
-
// 验证 invokeDefaultTypePlugin 是否没有被调用,因为类型是 standalone
|
|
282
397
|
expect(result.current.invokeDefaultTypePlugin).not.toHaveBeenCalled();
|
|
398
|
+
expect(result.current.invokeMarkdownTypePlugin).toHaveBeenCalled();
|
|
399
|
+
|
|
400
|
+
useChatStore.setState({ invokeMarkdownTypePlugin });
|
|
283
401
|
});
|
|
284
402
|
});
|
|
285
403
|
|
|
@@ -472,6 +590,7 @@ describe('ChatPluginAction', () => {
|
|
|
472
590
|
expect(result.current.toggleChatLoading).toHaveBeenCalledWith(false);
|
|
473
591
|
expect(useChatStore.getState().text2image).not.toHaveBeenCalled();
|
|
474
592
|
});
|
|
593
|
+
|
|
475
594
|
it('should handle errors when invoking a builtin tool fails', async () => {
|
|
476
595
|
const payload = {
|
|
477
596
|
apiName: 'builtinApi',
|
|
@@ -525,8 +644,10 @@ describe('ChatPluginAction', () => {
|
|
|
525
644
|
} as ChatPluginPayload;
|
|
526
645
|
const messageId = 'message-id';
|
|
527
646
|
|
|
528
|
-
|
|
529
|
-
|
|
647
|
+
const runPluginApiMock = vi.fn();
|
|
648
|
+
|
|
649
|
+
act(() => {
|
|
650
|
+
useChatStore.setState({ runPluginApi: runPluginApiMock });
|
|
530
651
|
});
|
|
531
652
|
|
|
532
653
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -536,7 +657,46 @@ describe('ChatPluginAction', () => {
|
|
|
536
657
|
});
|
|
537
658
|
|
|
538
659
|
// Verify that the markdown type plugin was invoked
|
|
539
|
-
expect(
|
|
660
|
+
expect(runPluginApiMock).toHaveBeenCalledWith(messageId, payload);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
describe('invokeStandaloneTypePlugin', () => {
|
|
665
|
+
it('should update message with error and refresh messages if plugin settings are invalid', async () => {
|
|
666
|
+
const messageId = 'message-id';
|
|
667
|
+
|
|
668
|
+
const payload = {
|
|
669
|
+
identifier: 'pluginName',
|
|
670
|
+
} as ChatPluginPayload;
|
|
671
|
+
|
|
672
|
+
act(() => {
|
|
673
|
+
useToolStore.setState({
|
|
674
|
+
validatePluginSettings: vi
|
|
675
|
+
.fn()
|
|
676
|
+
.mockResolvedValue({ valid: false, errors: ['Invalid setting'] }),
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
useChatStore.setState({ refreshMessages: vi.fn(), invokeStandaloneTypePlugin });
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const { result } = renderHook(() => useChatStore());
|
|
683
|
+
|
|
684
|
+
await act(async () => {
|
|
685
|
+
await result.current.invokeStandaloneTypePlugin(messageId, payload);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const call = vi.mocked(messageService.updateMessageError).mock.calls[0];
|
|
689
|
+
|
|
690
|
+
expect(call[1]).toEqual({
|
|
691
|
+
body: {
|
|
692
|
+
error: ['Invalid setting'],
|
|
693
|
+
message: '[plugin] your settings is invalid with plugin manifest setting schema',
|
|
694
|
+
},
|
|
695
|
+
message: undefined,
|
|
696
|
+
type: 'PluginSettingsInvalid',
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
540
700
|
});
|
|
541
701
|
});
|
|
542
702
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
2
|
+
import { t } from 'i18next';
|
|
1
3
|
import { Md5 } from 'ts-md5';
|
|
2
4
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
5
|
|
|
@@ -18,10 +20,15 @@ const n = setNamespace('plugin');
|
|
|
18
20
|
|
|
19
21
|
export interface ChatPluginAction {
|
|
20
22
|
createAssistantMessageByPlugin: (content: string, parentId: string) => Promise<void>;
|
|
21
|
-
fillPluginMessageContent: (
|
|
23
|
+
fillPluginMessageContent: (
|
|
24
|
+
id: string,
|
|
25
|
+
content: string,
|
|
26
|
+
triggerAiMessage?: boolean,
|
|
27
|
+
) => Promise<void>;
|
|
22
28
|
invokeBuiltinTool: (id: string, payload: ChatPluginPayload) => Promise<void>;
|
|
23
29
|
invokeDefaultTypePlugin: (id: string, payload: any) => Promise<void>;
|
|
24
30
|
invokeMarkdownTypePlugin: (id: string, payload: ChatPluginPayload) => Promise<void>;
|
|
31
|
+
invokeStandaloneTypePlugin: (id: string, payload: ChatPluginPayload) => Promise<void>;
|
|
25
32
|
runPluginApi: (id: string, payload: ChatPluginPayload) => Promise<string | undefined>;
|
|
26
33
|
triggerAIMessage: (id: string) => Promise<void>;
|
|
27
34
|
triggerFunctionCall: (id: string) => Promise<void>;
|
|
@@ -47,12 +54,12 @@ export const chatPlugin: StateCreator<
|
|
|
47
54
|
await get().refreshMessages();
|
|
48
55
|
},
|
|
49
56
|
|
|
50
|
-
fillPluginMessageContent: async (id, content) => {
|
|
57
|
+
fillPluginMessageContent: async (id, content, triggerAiMessage) => {
|
|
51
58
|
const { triggerAIMessage, updateMessageContent } = get();
|
|
52
59
|
|
|
53
60
|
await updateMessageContent(id, content);
|
|
54
61
|
|
|
55
|
-
await triggerAIMessage(id);
|
|
62
|
+
if (triggerAiMessage) await triggerAIMessage(id);
|
|
56
63
|
},
|
|
57
64
|
|
|
58
65
|
invokeBuiltinTool: async (id, payload) => {
|
|
@@ -103,6 +110,29 @@ export const chatPlugin: StateCreator<
|
|
|
103
110
|
await runPluginApi(id, payload);
|
|
104
111
|
},
|
|
105
112
|
|
|
113
|
+
invokeStandaloneTypePlugin: async (id, payload) => {
|
|
114
|
+
const result = await useToolStore.getState().validatePluginSettings(payload.identifier);
|
|
115
|
+
console.log('------------------');
|
|
116
|
+
console.log(result);
|
|
117
|
+
console.log('------------------');
|
|
118
|
+
if (!result) return;
|
|
119
|
+
|
|
120
|
+
// if the plugin settings is not valid, then set the message with error type
|
|
121
|
+
if (!result.valid) {
|
|
122
|
+
await messageService.updateMessageError(id, {
|
|
123
|
+
body: {
|
|
124
|
+
error: result.errors,
|
|
125
|
+
message: '[plugin] your settings is invalid with plugin manifest setting schema',
|
|
126
|
+
},
|
|
127
|
+
message: t('response.PluginSettingsInvalid', { ns: 'error' }),
|
|
128
|
+
type: PluginErrorType.PluginSettingsInvalid as any,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await get().refreshMessages();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
106
136
|
runPluginApi: async (id, payload) => {
|
|
107
137
|
const { updateMessageContent, refreshMessages, toggleChatLoading } = get();
|
|
108
138
|
let data: string;
|
|
@@ -144,6 +174,7 @@ export const chatPlugin: StateCreator<
|
|
|
144
174
|
const {
|
|
145
175
|
invokeDefaultTypePlugin,
|
|
146
176
|
invokeMarkdownTypePlugin,
|
|
177
|
+
invokeStandaloneTypePlugin,
|
|
147
178
|
invokeBuiltinTool,
|
|
148
179
|
refreshMessages,
|
|
149
180
|
} = get();
|
|
@@ -192,9 +223,10 @@ export const chatPlugin: StateCreator<
|
|
|
192
223
|
|
|
193
224
|
switch (payload.type) {
|
|
194
225
|
case 'standalone': {
|
|
195
|
-
|
|
226
|
+
await invokeStandaloneTypePlugin(id, payload);
|
|
196
227
|
break;
|
|
197
228
|
}
|
|
229
|
+
|
|
198
230
|
case 'markdown': {
|
|
199
231
|
await invokeMarkdownTypePlugin(id, payload);
|
|
200
232
|
break;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`useToolStore:plugin > validatePluginSettings > should return invalid result if settings do not match the schema 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"errors": [
|
|
6
|
+
{
|
|
7
|
+
"error": "Instance does not have required property "abc".",
|
|
8
|
+
"instanceLocation": "#",
|
|
9
|
+
"keyword": "required",
|
|
10
|
+
"keywordLocation": "#/required",
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
"valid": false,
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
|
|
1
2
|
import { act, renderHook } from '@testing-library/react';
|
|
2
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
|
|
4
5
|
import { pluginService } from '@/services/plugin';
|
|
6
|
+
import { LobeTool } from '@/types/tool';
|
|
5
7
|
|
|
6
8
|
import { useToolStore } from '../../store';
|
|
7
9
|
|
|
@@ -18,6 +20,64 @@ beforeEach(() => {
|
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
describe('useToolStore:plugin', () => {
|
|
23
|
+
describe('checkPluginsIsInstalled', () => {
|
|
24
|
+
it('should not perform any operations if the plugin list is empty', async () => {
|
|
25
|
+
const installPluginsMock = vi.fn();
|
|
26
|
+
useToolStore.setState({
|
|
27
|
+
loadPluginStore: vi.fn(),
|
|
28
|
+
installPlugins: installPluginsMock,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { result } = renderHook(() => useToolStore());
|
|
32
|
+
|
|
33
|
+
await act(async () => {
|
|
34
|
+
await result.current.checkPluginsIsInstalled([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(installPluginsMock).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should load the plugin store and install plugins if necessary', async () => {
|
|
41
|
+
const plugins = ['plugin1', 'plugin2'];
|
|
42
|
+
const loadPluginStoreMock = vi.fn();
|
|
43
|
+
const installPluginsMock = vi.fn();
|
|
44
|
+
useToolStore.setState({
|
|
45
|
+
loadPluginStore: loadPluginStoreMock,
|
|
46
|
+
installPlugins: installPluginsMock,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const { result } = renderHook(() => useToolStore());
|
|
50
|
+
|
|
51
|
+
await act(async () => {
|
|
52
|
+
await result.current.checkPluginsIsInstalled(plugins);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(loadPluginStoreMock).toHaveBeenCalled();
|
|
56
|
+
expect(installPluginsMock).toHaveBeenCalledWith(plugins);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should not load the plugin store and install plugins', async () => {
|
|
60
|
+
const plugins = ['plugin1', 'plugin2'];
|
|
61
|
+
const loadPluginStoreMock = vi.fn();
|
|
62
|
+
const installPluginsMock = vi.fn();
|
|
63
|
+
useToolStore.setState({
|
|
64
|
+
loadPluginStore: loadPluginStoreMock,
|
|
65
|
+
installPlugins: installPluginsMock,
|
|
66
|
+
installedPlugins: [{ identifier: 'abc' }] as LobeTool[],
|
|
67
|
+
pluginStoreList: [{ identifier: 'abc' }] as LobeChatPluginMeta[],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const { result } = renderHook(() => useToolStore());
|
|
71
|
+
|
|
72
|
+
await act(async () => {
|
|
73
|
+
await result.current.checkPluginsIsInstalled(plugins);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(loadPluginStoreMock).not.toHaveBeenCalled();
|
|
77
|
+
expect(installPluginsMock).toHaveBeenCalledWith(plugins);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
21
81
|
describe('updatePluginSettings', () => {
|
|
22
82
|
it('should update settings for a given plugin', async () => {
|
|
23
83
|
const pluginId = 'test-plugin';
|
|
@@ -44,4 +104,66 @@ describe('useToolStore:plugin', () => {
|
|
|
44
104
|
expect(pluginService.removeAllPlugins).toBeCalled();
|
|
45
105
|
});
|
|
46
106
|
});
|
|
107
|
+
|
|
108
|
+
describe('validatePluginSettings', () => {
|
|
109
|
+
// 模拟插件数据
|
|
110
|
+
const testPluginId = 'test-plugin';
|
|
111
|
+
// 定义测试用的 schema 和模拟的验证结果
|
|
112
|
+
const testSchema = {
|
|
113
|
+
properties: { abc: { type: 'string' } },
|
|
114
|
+
required: ['abc'],
|
|
115
|
+
type: 'object',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const testPluginSettings = { abc: 'valid-string' };
|
|
119
|
+
|
|
120
|
+
const testPlugin = {
|
|
121
|
+
type: 'plugin',
|
|
122
|
+
identifier: testPluginId,
|
|
123
|
+
manifest: {
|
|
124
|
+
identifier: testPluginId,
|
|
125
|
+
settings: testSchema,
|
|
126
|
+
},
|
|
127
|
+
settings: testPluginSettings,
|
|
128
|
+
} as unknown as LobeTool;
|
|
129
|
+
|
|
130
|
+
it('should validate settings against the schema and return valid result', async () => {
|
|
131
|
+
const { result } = renderHook(() => useToolStore());
|
|
132
|
+
|
|
133
|
+
act(() => {
|
|
134
|
+
useToolStore.setState({
|
|
135
|
+
installedPlugins: [testPlugin],
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const validationResult = await result.current.validatePluginSettings(testPluginId);
|
|
140
|
+
|
|
141
|
+
expect(validationResult).toEqual({ valid: true, errors: [] });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should return invalid result if settings do not match the schema', async () => {
|
|
145
|
+
const { result } = renderHook(() => useToolStore());
|
|
146
|
+
act(() => {
|
|
147
|
+
useToolStore.setState({
|
|
148
|
+
installedPlugins: [{ ...testPlugin, settings: {} }] as any,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const validationResult = await result.current.validatePluginSettings(testPluginId);
|
|
153
|
+
|
|
154
|
+
expect(validationResult).toMatchSnapshot();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return undefined if manifest or settings are not found', async () => {
|
|
158
|
+
useToolStore.setState({
|
|
159
|
+
installedPlugins: [],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const { result } = renderHook(() => useToolStore());
|
|
163
|
+
|
|
164
|
+
const validationResult = await result.current.validatePluginSettings(testPluginId);
|
|
165
|
+
|
|
166
|
+
expect(validationResult).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
47
169
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { Schema, ValidationResult } from '@cfworker/json-schema';
|
|
1
2
|
import useSWR, { SWRResponse } from 'swr';
|
|
2
3
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
4
|
|
|
4
5
|
import { pluginService } from '@/services/plugin';
|
|
5
|
-
import { pluginStoreSelectors } from '@/store/tool/selectors';
|
|
6
6
|
|
|
7
7
|
import { ToolStore } from '../../store';
|
|
8
|
+
import { pluginStoreSelectors } from '../store/selectors';
|
|
9
|
+
import { pluginSelectors } from './selectors';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* 插件接口
|
|
@@ -14,6 +16,7 @@ export interface PluginAction {
|
|
|
14
16
|
removeAllPlugins: () => Promise<void>;
|
|
15
17
|
updatePluginSettings: <T>(id: string, settings: Partial<T>) => Promise<void>;
|
|
16
18
|
useCheckPluginsIsInstalled: (plugins: string[]) => SWRResponse;
|
|
19
|
+
validatePluginSettings: (identifier: string) => Promise<ValidationResult | undefined>;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export const createPluginSlice: StateCreator<
|
|
@@ -45,4 +48,18 @@ export const createPluginSlice: StateCreator<
|
|
|
45
48
|
await get().refreshPlugins();
|
|
46
49
|
},
|
|
47
50
|
useCheckPluginsIsInstalled: (plugins) => useSWR(plugins, get().checkPluginsIsInstalled),
|
|
51
|
+
validatePluginSettings: async (identifier) => {
|
|
52
|
+
const manifest = pluginSelectors.getPluginManifestById(identifier)(get());
|
|
53
|
+
if (!manifest || !manifest.settings) return;
|
|
54
|
+
const settings = pluginSelectors.getPluginSettingsById(identifier)(get());
|
|
55
|
+
|
|
56
|
+
// validate the settings
|
|
57
|
+
const { Validator } = await import('@cfworker/json-schema');
|
|
58
|
+
const validator = new Validator(manifest.settings as Schema);
|
|
59
|
+
const result = validator.validate(settings);
|
|
60
|
+
|
|
61
|
+
if (!result.valid) return { errors: result.errors, valid: false };
|
|
62
|
+
|
|
63
|
+
return { errors: [], valid: true };
|
|
64
|
+
},
|
|
48
65
|
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# 数据存储取数模块
|
|
2
|
-
|
|
3
|
-
selectors 是 LobeChat 数据流研发框架下的取数模块,它的作用是从 store 中以特定特务逻辑取出数据,供组件消费使用。
|
|
4
|
-
|
|
5
|
-
以 `src/store/plugin/selectors.ts` 为例:
|
|
6
|
-
|
|
7
|
-
这个 TypeScript 代码段定义了一个名为 `pluginSelectors` 的对象,该对象包含一系列用于从插件存储状态中检索数据的选择器函数。选择器是一种从 Redux store(或类似的状态管理库)中提取和派生数据的函数。这个特定的例子是为了管理与前端应用程序的插件系统相关的状态。
|
|
8
|
-
|
|
9
|
-
下面是一些关键点的说明:
|
|
10
|
-
|
|
11
|
-
- `enabledSchema`: 一个函数,返回一个基于启用插件列表 `enabledPlugins` 过滤后的 `ChatCompletionFunctions` 数组。它将插件标识符作为前缀添加到 API 名称,以确保唯一性,并使用 `uniqBy` 函数从 Lodash 库中删除重复项。
|
|
12
|
-
- `onlinePluginStore`: 返回当前在线插件列表。
|
|
13
|
-
- `pluginList`: 返回插件列表,包括自定义插件和标准插件。
|
|
14
|
-
- `getPluginMetaById`: 根据插件 ID 返回插件元数据。
|
|
15
|
-
- `getDevPluginById`: 返回开发中的自定义插件信息。
|
|
16
|
-
- `getPluginManifestById`: 根据插件 ID 返回插件清单。
|
|
17
|
-
- `getPluginSettingsById`: 根据插件 ID 返回插件设置。
|
|
18
|
-
- `getPluginManifestLoadingStatus`: 根据插件 ID 返回插件清单的加载状态(加载中、成功或错误)。
|
|
19
|
-
- `isCustomPlugin`: 检查给定 ID 的插件是否为自定义插件。
|
|
20
|
-
- `displayPluginList`: 返回一个处理过的插件列表,包括作者、头像、创建时间、描述、首页 URL、标识符和标题。
|
|
21
|
-
- `hasPluginUI`: 根据插件 ID 确定插件是否有 UI 组件。
|
|
22
|
-
|
|
23
|
-
selectors 是高度模块化和可维护的,它通过将复杂的状态选择逻辑封装在单独的函数中,使得在应用程序的其他部分调用状态数据时,代码更加简洁和直观。此外,由于使用了 TypeScript,每个函数都可以具有明确的输入和输出类型,这有助于提高代码的可靠性和开发效率。
|
|
24
|
-
|
|
25
|
-
以 `displayPluginList` 方法为例。其代码如下:
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];
|
|
29
|
-
|
|
30
|
-
const displayPluginList = (s: PluginStoreState) =>
|
|
31
|
-
pluginList(s).map((p) => ({
|
|
32
|
-
author: p.author,
|
|
33
|
-
avatar: p.meta?.avatar,
|
|
34
|
-
createAt: p.createAt,
|
|
35
|
-
desc: pluginHelpers.getPluginDesc(p.meta),
|
|
36
|
-
homepage: p.homepage,
|
|
37
|
-
identifier: p.identifier,
|
|
38
|
-
title: pluginHelpers.getPluginTitle(p.meta),
|
|
39
|
-
}));
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
- `pluginList` 方法:用于从插件状态存储 PluginStoreState 中获取所有插件的列表。 它通过结合两个数组:pluginList 和 customPluginList 来创建一个新的插件列表;
|
|
43
|
-
- `displayPluginList` 方法:调用 pluginList 方法来获取合并后的插件列表,转换其中的 title 和 desc 变成显示在 UI 上的文本。
|
|
44
|
-
|
|
45
|
-
在组件中,只需引入便可直接获取最终消费的数据:
|
|
46
|
-
|
|
47
|
-
```tsx | pure
|
|
48
|
-
import { usePluginStore } from '@/store/plugin';
|
|
49
|
-
import { pluginSelectors } from '@/store/plugin/selectors';
|
|
50
|
-
|
|
51
|
-
const Render = ({ plugins }) => {
|
|
52
|
-
const list = usePluginStore(pluginSelectors.displayPluginList);
|
|
53
|
-
|
|
54
|
-
return <> ... </>;
|
|
55
|
-
};
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
这样实现的好处在于:
|
|
59
|
-
|
|
60
|
-
1. **解耦和重用**:通过将选择器独立于组件,我们可以在多个组件之间复用这些选择器而不需要重写取数逻辑。这减少了重复代码,提高了开发效率,并且使得代码库更加干净和易于维护。
|
|
61
|
-
2. **性能优化**:选择器可以用来计算派生数据,这样可以避免在每个组件中重复计算相同的数据。当状态发生变化时,只有依赖于这部分状态的选择器才会重新计算,从而减少不必要的渲染和计算。
|
|
62
|
-
3. **易于测试**:选择器是纯函数,它们仅依赖于传入的参数。这意味着它们可以在隔离的环境中进行测试,无需模拟整个 store 或组件树。
|
|
63
|
-
4. **类型安全**:由于 LobeChat 使用 TypeScript,每个选择器都有明确的输入和输出类型定义。这为开发者提供了自动完成和编译时检查的优势,减少了运行时错误。
|
|
64
|
-
5. **可维护性**:选择器集中了状态的读取逻辑,使得跟踪状态的变化和管理更加直观。如果状态结构发生变化,我们只需要更新相应的选择器,而不是搜索和替换整个代码库中的多个位置。
|
|
65
|
-
6. **可组合性**:选择器可以组合其他选择器,以创建更复杂的选择逻辑。这种模式允许开发者构建一个选择器层次结构,使得状态选择更加灵活和强大。
|
|
66
|
-
7. **简化组件逻辑**:组件不需要知道状态的结构或如何获取和计算需要的数据。组件只需调用选择器即可获取渲染所需的数据,这使得组件逻辑变得更简单和清晰。
|
|
67
|
-
|
|
68
|
-
通过这样的设计,LobeChat 的开发者可以更专注于构建用户界面和业务逻辑,而不必担心数据的获取和处理细节。这种模式也为未来可能的状态结构变更提供了更好的适应性和扩展性。
|
|
File without changes
|
|
File without changes
|
|
File without changes
|