@lobehub/chat 0.120.1 → 0.120.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +47 -22
  3. package/README.zh-CN.md +53 -26
  4. package/docs/Deployment/Upstream-Sync.md +8 -2
  5. package/docs/Deployment/Upstream-Sync.zh-CN.md +9 -3
  6. package/docs/Development/Add-New-Locale.md +9 -1
  7. package/docs/Development/Add-New-Locale.zh-CN.md +9 -1
  8. package/docs/Development/Contributing-Guidelines.md +83 -0
  9. package/docs/Development/Contributing-Guidelines.zh-CN.md +13 -0
  10. package/docs/Development/Internationalization-Implementation.md +125 -0
  11. package/docs/Development/Internationalization-Implementation.zh-CN.md +9 -1
  12. package/docs/Development/State-Management/index.md +224 -0
  13. package/docs/Development/State-Management/index.zh-CN.md +25 -28
  14. package/docs/Home.md +9 -6
  15. package/docs/_Sidebar.md +9 -6
  16. package/locales/ar/common.json +1 -0
  17. package/locales/ar/setting.json +1 -3
  18. package/locales/de-DE/common.json +1 -0
  19. package/locales/de-DE/setting.json +1 -3
  20. package/locales/en-US/common.json +1 -0
  21. package/locales/en-US/setting.json +1 -3
  22. package/locales/es-ES/common.json +1 -0
  23. package/locales/es-ES/setting.json +1 -3
  24. package/locales/fr-FR/common.json +1 -0
  25. package/locales/fr-FR/setting.json +1 -3
  26. package/locales/it-IT/common.json +1 -0
  27. package/locales/it-IT/setting.json +1 -3
  28. package/locales/ja-JP/common.json +1 -0
  29. package/locales/ja-JP/setting.json +1 -3
  30. package/locales/ko-KR/common.json +1 -0
  31. package/locales/ko-KR/setting.json +1 -3
  32. package/locales/nl-NL/common.json +1 -0
  33. package/locales/nl-NL/setting.json +1 -3
  34. package/locales/pl-PL/common.json +1 -0
  35. package/locales/pl-PL/setting.json +1 -3
  36. package/locales/pt-BR/common.json +1 -0
  37. package/locales/pt-BR/setting.json +1 -3
  38. package/locales/ru-RU/common.json +1 -0
  39. package/locales/ru-RU/setting.json +1 -3
  40. package/locales/tr-TR/common.json +1 -0
  41. package/locales/tr-TR/setting.json +1 -3
  42. package/locales/vi-VN/common.json +1 -0
  43. package/locales/vi-VN/setting.json +1 -3
  44. package/locales/zh-CN/common.json +1 -0
  45. package/locales/zh-CN/setting.json +1 -3
  46. package/locales/zh-TW/common.json +1 -0
  47. package/locales/zh-TW/setting.json +1 -3
  48. package/next.config.mjs +2 -2
  49. package/package.json +7 -6
  50. package/scripts/docsWorkflow/const.ts +1 -1
  51. package/scripts/docsWorkflow/index.ts +9 -3
  52. package/scripts/readmeWorkflow/syncAgentIndex.ts +2 -2
  53. package/scripts/readmeWorkflow/syncPluginIndex.ts +2 -2
  54. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +0 -2
  55. package/src/app/settings/common/Common.tsx +5 -6
  56. package/src/const/locale.ts +2 -2
  57. package/src/features/ChatInput/ActionBar/Clear.tsx +0 -2
  58. package/src/features/Conversation/Plugins/Render/StandaloneType/Iframe.tsx +2 -2
  59. package/src/features/Conversation/Plugins/Render/utils/listenToPlugin.test.ts +2 -2
  60. package/src/features/Conversation/Plugins/Render/utils/listenToPlugin.ts +5 -2
  61. package/src/features/PluginDetailModal/index.tsx +0 -2
  62. package/src/features/PluginDevModal/index.tsx +0 -1
  63. package/src/layout/GlobalLayout/Locale.tsx +37 -10
  64. package/src/layout/GlobalLayout/index.tsx +1 -1
  65. package/src/locales/default/setting.ts +0 -2
  66. package/src/locales/resources.ts +9 -7
  67. package/src/store/chat/slices/tool/action.test.ts +167 -7
  68. package/src/store/chat/slices/tool/action.ts +36 -4
  69. package/src/store/tool/slices/plugin/__snapshots__/action.test.ts.snap +15 -0
  70. package/src/store/tool/slices/plugin/action.test.ts +122 -0
  71. package/src/store/tool/slices/plugin/action.ts +18 -1
@@ -252,9 +252,7 @@ export default {
252
252
  fontSize: {
253
253
  desc: '聊天内容的字体大小',
254
254
  marks: {
255
- large: '大',
256
255
  normal: '标准',
257
- small: '小',
258
256
  },
259
257
  title: '字体大小',
260
258
  },
@@ -19,7 +19,9 @@ export const locales = [
19
19
  export type DefaultResources = typeof resources;
20
20
  export type Locales = (typeof locales)[number];
21
21
 
22
- export const normalizeLocale = (locale: string) => {
22
+ export const normalizeLocale = (locale?: string) => {
23
+ if (!locale) return 'en-US';
24
+
23
25
  switch (locale) {
24
26
  case 'zh-CN':
25
27
  case 'zh': {
@@ -43,8 +45,8 @@ type LocaleOptions = {
43
45
 
44
46
  export const localeOptions: LocaleOptions = [
45
47
  {
46
- label: 'العربية',
47
- value: 'ar',
48
+ label: 'English',
49
+ value: 'en-US',
48
50
  },
49
51
  {
50
52
  label: '简体中文',
@@ -54,10 +56,6 @@ export const localeOptions: LocaleOptions = [
54
56
  label: '繁體中文',
55
57
  value: 'zh-TW',
56
58
  },
57
- {
58
- label: 'English',
59
- value: 'en-US',
60
- },
61
59
  {
62
60
  label: '日本語',
63
61
  value: 'ja-JP',
@@ -74,6 +72,10 @@ export const localeOptions: LocaleOptions = [
74
72
  label: 'Español',
75
73
  value: 'es-ES',
76
74
  },
75
+ {
76
+ label: 'العربية',
77
+ value: 'ar',
78
+ },
77
79
  {
78
80
  label: 'Français',
79
81
  value: 'fr-FR',
@@ -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 process the message', async () => {
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
- invokeDefaultTypePlugin: vi.fn(),
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
- useChatStore.setState({
529
- runPluginApi: vi.fn().mockResolvedValue('Markdown response'),
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(result.current.runPluginApi).toHaveBeenCalledWith(messageId, payload);
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: (id: string, content: string) => Promise<void>;
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
- // TODO: need to auth user's settings
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
  });