@lobehub/chat 0.120.1 → 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 +25 -0
- package/package.json +2 -1
- 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/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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.120.2](https://github.com/lobehub/lobe-chat/compare/v0.120.1...v0.120.2)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-01-17**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix setPluginMessage can not stop create ai message.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix setPluginMessage can not stop create ai message, closes [#1078](https://github.com/lobehub/lobe-chat/issues/1078) ([67de28d](https://github.com/lobehub/lobe-chat/commit/67de28d))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 0.120.1](https://github.com/lobehub/lobe-chat/compare/v0.120.0...v0.120.1)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2024-01-16**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.120.
|
|
3
|
+
"version": "0.120.2",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@ant-design/icons": "^5",
|
|
79
|
+
"@cfworker/json-schema": "^1",
|
|
79
80
|
"@icons-pack/react-simple-icons": "^9",
|
|
80
81
|
"@lobehub/chat-plugin-sdk": "latest",
|
|
81
82
|
"@lobehub/chat-plugins-gateway": "latest",
|
|
@@ -77,8 +77,8 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
|
|
|
77
77
|
|
|
78
78
|
// when plugin try to send back message, we should fill it to the message content
|
|
79
79
|
const fillPluginContent = useChatStore((s) => s.fillPluginMessageContent);
|
|
80
|
-
useOnPluginFillContent((content) => {
|
|
81
|
-
fillPluginContent(id, content);
|
|
80
|
+
useOnPluginFillContent((content, triggerAiMessage) => {
|
|
81
|
+
fillPluginContent(id, content, triggerAiMessage);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
// when plugin wants to get plugin state, send it to plugin
|
|
@@ -72,7 +72,7 @@ describe('useOnPluginFillContent', () => {
|
|
|
72
72
|
|
|
73
73
|
window.dispatchEvent(event);
|
|
74
74
|
|
|
75
|
-
expect(mockCallback).toHaveBeenCalledWith(testContent);
|
|
75
|
+
expect(mockCallback).toHaveBeenCalledWith(testContent, undefined);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
it('calls callback with JSON stringified content if content is not a string', () => {
|
|
@@ -86,7 +86,7 @@ describe('useOnPluginFillContent', () => {
|
|
|
86
86
|
|
|
87
87
|
window.dispatchEvent(event);
|
|
88
88
|
|
|
89
|
-
expect(mockCallback).toHaveBeenCalledWith(JSON.stringify(testContent));
|
|
89
|
+
expect(mockCallback).toHaveBeenCalledWith(JSON.stringify(testContent), undefined);
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
92
|
|
|
@@ -31,14 +31,17 @@ export const useOnPluginFetchPluginState = (onRequest: (key: string) => void) =>
|
|
|
31
31
|
}, []);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
export const useOnPluginFillContent = (
|
|
34
|
+
export const useOnPluginFillContent = (
|
|
35
|
+
callback: (content: string, triggerAiMessage?: boolean) => void,
|
|
36
|
+
) => {
|
|
35
37
|
useEffect(() => {
|
|
36
38
|
const fn = (e: MessageEvent) => {
|
|
37
39
|
if (e.data.type === PluginChannel.fillStandalonePluginContent) {
|
|
38
40
|
const data = e.data.content;
|
|
41
|
+
const triggerAiMessage = e.data.triggerAiMessage;
|
|
39
42
|
const content = typeof data !== 'string' ? JSON.stringify(data) : data;
|
|
40
43
|
|
|
41
|
-
callback(content);
|
|
44
|
+
callback(content, triggerAiMessage);
|
|
42
45
|
}
|
|
43
46
|
};
|
|
44
47
|
|
|
@@ -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
|
});
|