@lobehub/chat 0.114.9 → 0.115.1

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 (88) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/locales/de-DE/tool.json +8 -0
  3. package/locales/en-US/tool.json +8 -0
  4. package/locales/es-ES/tool.json +8 -0
  5. package/locales/fr-FR/tool.json +8 -0
  6. package/locales/it-IT/tool.json +8 -0
  7. package/locales/ja-JP/tool.json +8 -0
  8. package/locales/ko-KR/tool.json +8 -0
  9. package/locales/nl-NL/tool.json +8 -0
  10. package/locales/pl-PL/tool.json +8 -0
  11. package/locales/pt-BR/tool.json +8 -0
  12. package/locales/ru-RU/tool.json +8 -0
  13. package/locales/tr-TR/tool.json +8 -0
  14. package/locales/zh-CN/plugin.json +2 -2
  15. package/locales/zh-CN/setting.json +1 -1
  16. package/locales/zh-CN/tool.json +8 -0
  17. package/locales/zh-TW/tool.json +8 -0
  18. package/package.json +2 -2
  19. package/src/app/api/openai/images/createImageGeneration.ts +26 -0
  20. package/src/app/api/openai/images/route.ts +16 -0
  21. package/src/app/api/proxy/route.ts +3 -1
  22. package/src/app/chat/(desktop)/features/ChatHeader.tsx +56 -56
  23. package/src/app/chat/features/ChatHeader/PluginTag/PluginStatus.tsx +7 -4
  24. package/src/app/chat/features/ChatHeader/PluginTag/index.tsx +3 -3
  25. package/src/app/chat/features/SystemRole/index.tsx +0 -1
  26. package/src/components/GalleyGrid/Grid.tsx +36 -0
  27. package/src/components/GalleyGrid/index.tsx +58 -0
  28. package/src/components/GalleyGrid/style.ts +24 -0
  29. package/src/const/settings.ts +7 -0
  30. package/src/database/schemas/files.ts +1 -0
  31. package/src/database/schemas/message.ts +1 -1
  32. package/src/features/AgentSetting/AgentPlugin/index.tsx +2 -2
  33. package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +2 -1
  34. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +3 -3
  35. package/src/features/ChatInput/ActionBar/Tools/index.tsx +26 -14
  36. package/src/features/Conversation/ChatList/Messages/Function.tsx +1 -1
  37. package/src/features/Conversation/ChatList/Plugins/Inspector/index.tsx +4 -2
  38. package/src/features/Conversation/ChatList/Plugins/Render/BuiltinType/index.tsx +36 -0
  39. package/src/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx +6 -66
  40. package/src/features/Conversation/ChatList/Plugins/Render/Loading.tsx +61 -0
  41. package/src/features/Conversation/ChatList/Plugins/Render/index.tsx +21 -12
  42. package/src/features/Conversation/ChatList/Plugins/Render/useParseContent.ts +15 -0
  43. package/src/locales/default/index.ts +2 -0
  44. package/src/locales/default/plugin.ts +2 -2
  45. package/src/locales/default/setting.ts +1 -1
  46. package/src/locales/default/tool.ts +9 -0
  47. package/src/services/__tests__/chat.test.ts +132 -2
  48. package/src/services/_url.ts +1 -0
  49. package/src/services/chat.ts +3 -5
  50. package/src/services/file.ts +20 -5
  51. package/src/services/imageGeneration.ts +26 -0
  52. package/src/store/chat/actions/enhance.ts +65 -1
  53. package/src/store/chat/actions/plugin.test.ts +9 -9
  54. package/src/store/chat/actions/plugin.ts +38 -6
  55. package/src/store/chat/initialState.ts +3 -0
  56. package/src/store/chat/selectors/enchance.ts +11 -0
  57. package/src/store/chat/selectors/index.ts +1 -0
  58. package/src/store/global/selectors/__snapshots__/settings.test.ts.snap +5 -0
  59. package/src/store/global/selectors/settings.ts +5 -0
  60. package/src/store/tool/helpers.ts +6 -5
  61. package/src/store/tool/initialState.ts +3 -1
  62. package/src/store/tool/selectors/index.ts +5 -0
  63. package/src/store/tool/selectors/tool.test.ts +128 -0
  64. package/src/store/tool/selectors/tool.ts +125 -0
  65. package/src/store/tool/slices/builtin/action.ts +54 -0
  66. package/src/store/tool/slices/builtin/index.ts +2 -0
  67. package/src/store/tool/slices/builtin/initialState.ts +13 -0
  68. package/src/store/tool/slices/builtin/selectors.test.ts +70 -0
  69. package/src/store/tool/slices/builtin/selectors.ts +20 -0
  70. package/src/store/tool/slices/customPlugin/action.ts +0 -3
  71. package/src/store/tool/slices/plugin/selectors.test.ts +0 -69
  72. package/src/store/tool/slices/plugin/selectors.ts +1 -84
  73. package/src/store/tool/store.ts +8 -2
  74. package/src/tools/dalle/Render/EditMode.tsx +66 -0
  75. package/src/tools/dalle/Render/Item.tsx +97 -0
  76. package/src/tools/dalle/Render/ToolBar.tsx +54 -0
  77. package/src/tools/dalle/Render/index.tsx +21 -0
  78. package/src/tools/dalle/index.ts +81 -0
  79. package/src/tools/index.ts +11 -0
  80. package/src/tools/renders.ts +8 -0
  81. package/src/types/chatMessage.ts +1 -1
  82. package/src/types/openai/image.ts +34 -0
  83. package/src/types/settings.ts +6 -0
  84. package/src/types/tool/builtin.ts +36 -0
  85. package/src/types/tool/dalle.ts +12 -0
  86. package/src/types/tool/index.ts +5 -1
  87. package/src/types/tool/tool.ts +9 -0
  88. package/src/store/tool/selectors.ts +0 -3
@@ -1,5 +1,6 @@
1
1
  import { FileModel } from '@/database/models/file';
2
2
  import { DB_File } from '@/database/schemas/files';
3
+ import { PROXY_URL } from '@/services/_url';
3
4
  import { FilePreview } from '@/types/files';
4
5
  import compressImage from '@/utils/compressImage';
5
6
 
@@ -14,6 +15,7 @@ class FileService {
14
15
  if (this.isImage(file.fileType) && !isTestData) {
15
16
  return this.uploadImageFile(file);
16
17
  }
18
+
17
19
  // save to local storage
18
20
  // we may want to save to a remote server later
19
21
  return FileModel.create(file);
@@ -22,6 +24,7 @@ class FileService {
22
24
  async uploadImageFile(file: DB_File) {
23
25
  // 加载图片
24
26
  const url = file.url || URL.createObjectURL(new Blob([file.data]));
27
+
25
28
  const img = new Image();
26
29
  img.src = url;
27
30
  await (() =>
@@ -31,10 +34,7 @@ class FileService {
31
34
 
32
35
  // 压缩图片
33
36
  const fileType = 'image/webp';
34
- const base64String = compressImage({
35
- img,
36
- type: fileType,
37
- });
37
+ const base64String = compressImage({ img, type: fileType });
38
38
  const binaryString = atob(base64String.split('base64,')[1]);
39
39
  const uint8Array = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
40
40
  file.data = uint8Array.buffer;
@@ -42,6 +42,21 @@ class FileService {
42
42
  return FileModel.create(file);
43
43
  }
44
44
 
45
+ async uploadImageByUrl(url: string, file: Pick<DB_File, 'name' | 'metadata'>) {
46
+ const res = await fetch(PROXY_URL, { body: url, method: 'POST' });
47
+ const data = await res.arrayBuffer();
48
+ const fileType = res.headers.get('content-type') || 'image/webp';
49
+
50
+ return this.uploadFile({
51
+ data,
52
+ fileType,
53
+ metadata: file.metadata,
54
+ name: file.name,
55
+ saveMode: 'local',
56
+ size: data.byteLength,
57
+ });
58
+ }
59
+
45
60
  async removeFile(id: string) {
46
61
  return FileModel.delete(id);
47
62
  }
@@ -57,7 +72,7 @@ class FileService {
57
72
  }
58
73
 
59
74
  // arrayBuffer to url
60
- const url = URL.createObjectURL(new Blob([item.data]));
75
+ const url = URL.createObjectURL(new Blob([item.data], { type: item.fileType }));
61
76
  const base64 = Buffer.from(item.data).toString('base64');
62
77
 
63
78
  return {
@@ -0,0 +1,26 @@
1
+ import { createHeaderWithOpenAI } from '@/services/_header';
2
+ import { OPENAI_URLS } from '@/services/_url';
3
+ import { OpenAIImagePayload } from '@/types/openai/image';
4
+
5
+ interface FetchOptions {
6
+ signal?: AbortSignal | undefined;
7
+ }
8
+
9
+ class ImageGenerationService {
10
+ async generateImage(params: Omit<OpenAIImagePayload, 'model' | 'n'>, options?: FetchOptions) {
11
+ const payload: OpenAIImagePayload = { ...params, model: 'dall-e-3', n: 1 };
12
+
13
+ const res = await fetch(OPENAI_URLS.images, {
14
+ body: JSON.stringify(payload),
15
+ headers: createHeaderWithOpenAI({ 'Content-Type': 'application/json' }),
16
+ method: 'POST',
17
+ signal: options?.signal,
18
+ });
19
+
20
+ const urls = await res.json();
21
+
22
+ return urls[0] as string;
23
+ }
24
+ }
25
+
26
+ export const imageGenerationService = new ImageGenerationService();
@@ -1,13 +1,17 @@
1
1
  import { produce } from 'immer';
2
+ import pMap from 'p-map';
2
3
  import { StateCreator } from 'zustand/vanilla';
3
4
 
4
5
  import { chainLangDetect } from '@/chains/langDetect';
5
6
  import { chainTranslate } from '@/chains/translate';
6
7
  import { supportLocales } from '@/locales/options';
7
8
  import { chatService } from '@/services/chat';
9
+ import { fileService } from '@/services/file';
10
+ import { imageGenerationService } from '@/services/imageGeneration';
8
11
  import { messageService } from '@/services/message';
9
12
  import { chatSelectors } from '@/store/chat/selectors';
10
13
  import { ChatStore } from '@/store/chat/store';
14
+ import { DallEImageItem } from '@/types/tool/dalle';
11
15
  import { setNamespace } from '@/utils/storeDebug';
12
16
 
13
17
  const n = setNamespace('enhance');
@@ -17,13 +21,16 @@ const n = setNamespace('enhance');
17
21
  */
18
22
  export interface ChatEnhanceAction {
19
23
  clearTTS: (id: string) => Promise<void>;
20
-
21
24
  clearTranslate: (id: string) => Promise<void>;
25
+ generateImageFromPrompts: (items: DallEImageItem[], id: string) => Promise<void>;
26
+ text2image: (id: string, data: DallEImageItem[]) => Promise<void>;
27
+ toggleDallEImageLoading: (key: string, value: boolean) => void;
22
28
  translateMessage: (id: string, targetLang: string) => Promise<void>;
23
29
  ttsMessage: (
24
30
  id: string,
25
31
  state?: { contentMd5?: string; file?: string; voice?: string },
26
32
  ) => Promise<void>;
33
+ updateImageItem: (id: string, updater: (data: DallEImageItem[]) => void) => Promise<void>;
27
34
  }
28
35
 
29
36
  export const chatEnhance: StateCreator<
@@ -42,6 +49,52 @@ export const chatEnhance: StateCreator<
42
49
  await get().refreshMessages();
43
50
  },
44
51
 
52
+ generateImageFromPrompts: async (items, messageId) => {
53
+ const { toggleDallEImageLoading, updateImageItem } = get();
54
+ // eslint-disable-next-line unicorn/consistent-function-scoping
55
+ const getMessageById = (id: string) => chatSelectors.getMessageById(id)(get());
56
+
57
+ const message = getMessageById(messageId);
58
+ const parent = getMessageById(message!.parentId!);
59
+ const originPrompt = parent?.content;
60
+
61
+ await pMap(items, async (params, index) => {
62
+ toggleDallEImageLoading(messageId + params.prompt, true);
63
+ const url = await imageGenerationService.generateImage(params);
64
+
65
+ await updateImageItem(messageId, (draft) => {
66
+ draft[index].previewUrl = url;
67
+ });
68
+
69
+ toggleDallEImageLoading(messageId + params.prompt, false);
70
+
71
+ fileService
72
+ .uploadImageByUrl(url, {
73
+ metadata: { ...params, originPrompt: originPrompt },
74
+ name: `${originPrompt || params.prompt}_${index}.png`,
75
+ })
76
+ .then(({ id }) => {
77
+ updateImageItem(messageId, (draft) => {
78
+ draft[index].imageId = id;
79
+ draft[index].previewUrl = undefined;
80
+ });
81
+ });
82
+ });
83
+ },
84
+ text2image: async (id, data) => {
85
+ // const isAutoGen = settingsSelectors.isDalleAutoGenerating(useGlobalStore.getState());
86
+ // if (!isAutoGen) return;
87
+
88
+ await get().generateImageFromPrompts(data, id);
89
+ },
90
+ toggleDallEImageLoading: (key, value) => {
91
+ set(
92
+ { dalleImageLoading: { ...get().dalleImageLoading, [key]: value } },
93
+ false,
94
+ n('toggleDallEImageLoading'),
95
+ );
96
+ },
97
+
45
98
  translateMessage: async (id, targetLang) => {
46
99
  const { toggleChatLoading, dispatchMessage, refreshMessages } = get();
47
100
 
@@ -95,4 +148,15 @@ export const chatEnhance: StateCreator<
95
148
  await messageService.updateMessageTTS(id, state);
96
149
  await get().refreshMessages();
97
150
  },
151
+
152
+ updateImageItem: async (id, updater) => {
153
+ const message = chatSelectors.getMessageById(id)(get());
154
+ if (!message) return;
155
+
156
+ const data: DallEImageItem[] = JSON.parse(message.content);
157
+
158
+ const nextContent = produce(data, updater);
159
+ await messageService.updateMessageContent(id, JSON.stringify(nextContent));
160
+ await get().refreshMessages();
161
+ },
98
162
  });
@@ -86,7 +86,7 @@ describe('ChatPluginAction', () => {
86
86
  const { result } = renderHook(() => useChatStore());
87
87
 
88
88
  await act(async () => {
89
- await result.current.runPluginDefaultType(messageId, pluginPayload);
89
+ await result.current.invokeDefaultTypePlugin(messageId, pluginPayload);
90
90
  });
91
91
 
92
92
  expect(initialState.toggleChatLoading).toHaveBeenCalledWith(
@@ -121,7 +121,7 @@ describe('ChatPluginAction', () => {
121
121
  const { result } = renderHook(() => useChatStore());
122
122
 
123
123
  await act(async () => {
124
- await result.current.runPluginDefaultType(messageId, pluginPayload);
124
+ await result.current.invokeDefaultTypePlugin(messageId, pluginPayload);
125
125
  });
126
126
 
127
127
  expect(initialState.toggleChatLoading).toHaveBeenCalledWith(
@@ -150,7 +150,7 @@ describe('ChatPluginAction', () => {
150
150
 
151
151
  const initialState = {
152
152
  refreshMessages: vi.fn(),
153
- runPluginDefaultType: vi.fn(),
153
+ invokeDefaultTypePlugin: vi.fn(),
154
154
  };
155
155
  useChatStore.setState(initialState);
156
156
 
@@ -172,7 +172,7 @@ describe('ChatPluginAction', () => {
172
172
  role: 'function',
173
173
  });
174
174
  expect(initialState.refreshMessages).toHaveBeenCalled();
175
- expect(initialState.runPluginDefaultType).toHaveBeenCalledWith(
175
+ expect(initialState.invokeDefaultTypePlugin).toHaveBeenCalledWith(
176
176
  messageId,
177
177
  messagePluginPayload,
178
178
  );
@@ -191,7 +191,7 @@ describe('ChatPluginAction', () => {
191
191
 
192
192
  useToolStore.setState({ installedPlugins: [plugin] });
193
193
 
194
- useChatStore.setState({ refreshMessages: vi.fn(), runPluginDefaultType: vi.fn() });
194
+ useChatStore.setState({ refreshMessages: vi.fn(), invokeDefaultTypePlugin: vi.fn() });
195
195
 
196
196
  (chatSelectors.getMessageById as Mock).mockImplementation(() => () => ({
197
197
  id: messageId,
@@ -212,7 +212,7 @@ describe('ChatPluginAction', () => {
212
212
  plugin: expect.objectContaining({ apiName }),
213
213
  }),
214
214
  );
215
- expect(result.current.runPluginDefaultType).toHaveBeenCalledWith(
215
+ expect(result.current.invokeDefaultTypePlugin).toHaveBeenCalledWith(
216
216
  messageId,
217
217
  expect.objectContaining({
218
218
  apiName: apiName,
@@ -231,7 +231,7 @@ describe('ChatPluginAction', () => {
231
231
 
232
232
  useChatStore.setState({
233
233
  refreshMessages: vi.fn(),
234
- runPluginDefaultType: vi.fn(),
234
+ invokeDefaultTypePlugin: vi.fn(),
235
235
  });
236
236
 
237
237
  (chatSelectors.getMessageById as Mock).mockImplementation(() => () => ({
@@ -248,8 +248,8 @@ describe('ChatPluginAction', () => {
248
248
  // 验证 refreshMessages 是否被调用
249
249
  expect(result.current.refreshMessages).toHaveBeenCalled();
250
250
 
251
- // 验证 runPluginDefaultType 是否没有被调用,因为类型是 standalone
252
- expect(result.current.runPluginDefaultType).not.toHaveBeenCalled();
251
+ // 验证 invokeDefaultTypePlugin 是否没有被调用,因为类型是 standalone
252
+ expect(result.current.invokeDefaultTypePlugin).not.toHaveBeenCalled();
253
253
  });
254
254
  });
255
255
 
@@ -17,7 +17,8 @@ const n = setNamespace('plugin');
17
17
 
18
18
  export interface ChatPluginAction {
19
19
  fillPluginMessageContent: (id: string, content: string) => Promise<void>;
20
- runPluginDefaultType: (id: string, payload: any) => Promise<void>;
20
+ invokeBuiltinTool: (id: string, payload: ChatPluginPayload) => Promise<void>;
21
+ invokeDefaultTypePlugin: (id: string, payload: any) => Promise<void>;
21
22
  triggerFunctionCall: (id: string) => Promise<void>;
22
23
  updatePluginState: (id: string, key: string, value: any) => Promise<void>;
23
24
  }
@@ -37,7 +38,26 @@ export const chatPlugin: StateCreator<
37
38
  const chats = chatSelectors.currentChats(get());
38
39
  await coreProcessMessage(chats, id);
39
40
  },
40
- runPluginDefaultType: async (id, payload) => {
41
+ invokeBuiltinTool: async (id, payload) => {
42
+ const { toggleChatLoading, refreshMessages } = get();
43
+ const params = JSON.parse(payload.arguments);
44
+ toggleChatLoading(true, id, n('invokeBuiltinTool') as string);
45
+ const data = await useToolStore.getState().invokeBuiltinTool(payload.apiName, params);
46
+ toggleChatLoading(false);
47
+
48
+ if (data) {
49
+ await messageService.updateMessageContent(id, data);
50
+ await refreshMessages();
51
+ }
52
+
53
+ // postToolCalling
54
+ // @ts-ignore
55
+ const { [payload.apiName]: action } = get();
56
+ if (!action || !data) return;
57
+
58
+ await action(id, JSON.parse(data));
59
+ },
60
+ invokeDefaultTypePlugin: async (id, payload) => {
41
61
  const { refreshMessages, coreProcessMessage, toggleChatLoading } = get();
42
62
  let data: string;
43
63
 
@@ -65,8 +85,9 @@ export const chatPlugin: StateCreator<
65
85
  const chats = chatSelectors.currentChats(get());
66
86
  await coreProcessMessage(chats, id);
67
87
  },
88
+
68
89
  triggerFunctionCall: async (id) => {
69
- const { runPluginDefaultType, refreshMessages } = get();
90
+ const { invokeDefaultTypePlugin, invokeBuiltinTool, refreshMessages } = get();
70
91
 
71
92
  const message = chatSelectors.getMessageById(id)(get());
72
93
  if (!message) return;
@@ -111,10 +132,21 @@ export const chatPlugin: StateCreator<
111
132
  });
112
133
  await refreshMessages();
113
134
 
114
- if (payload.type === 'standalone') {
115
- // TODO: need to auth user's settings
116
- } else runPluginDefaultType(id, payload);
135
+ switch (payload.type) {
136
+ case 'standalone': {
137
+ // TODO: need to auth user's settings
138
+ break;
139
+ }
140
+ case 'builtin': {
141
+ await invokeBuiltinTool(id, payload);
142
+ break;
143
+ }
144
+ default: {
145
+ await invokeDefaultTypePlugin(id, payload);
146
+ }
147
+ }
117
148
  },
149
+
118
150
  updatePluginState: async (id, key, value) => {
119
151
  const { refreshMessages } = get();
120
152
 
@@ -13,6 +13,8 @@ export interface ChatStoreState {
13
13
  * @deprecated
14
14
  */
15
15
  chatLoadingId?: string;
16
+ dalleImageLoading: Record<string, boolean>;
17
+
16
18
  inputMessage: string;
17
19
  isSearchingTopic: boolean;
18
20
  messageLoadingIds: [];
@@ -36,6 +38,7 @@ export interface ChatStoreState {
36
38
 
37
39
  export const initialState: ChatStoreState = {
38
40
  activeId: 'inbox',
41
+ dalleImageLoading: {},
39
42
  inputMessage: '',
40
43
  isSearchingTopic: false,
41
44
  messageLoadingIds: [],
@@ -0,0 +1,11 @@
1
+ import { ChatStoreState } from '@/store/chat';
2
+
3
+ const isDallEImageGenerating = (id: string) => (s: ChatStoreState) => s.dalleImageLoading[id];
4
+
5
+ const isGeneratingDallEImage = (s: ChatStoreState) =>
6
+ Object.values(s.dalleImageLoading).some(Boolean);
7
+
8
+ export const chatEnhanceSelectors = {
9
+ isDallEImageGenerating,
10
+ isGeneratingDallEImage,
11
+ };
@@ -1,2 +1,3 @@
1
1
  export { chatSelectors } from './chat';
2
+ export { chatEnhanceSelectors } from './enchance';
2
3
  export { topicSelectors } from './topic';
@@ -147,6 +147,11 @@ exports[`settingsSelectors > currentSettings > should merge DEFAULT_SETTINGS and
147
147
  "password": "password123",
148
148
  "primaryColor": "blue",
149
149
  "themeMode": "light",
150
+ "tool": {
151
+ "dalle": {
152
+ "autoGenerate": false,
153
+ },
154
+ },
150
155
  "tts": {
151
156
  "openAI": {
152
157
  "sttModel": "whisper-1",
@@ -84,14 +84,19 @@ const currentLanguage = (s: GlobalStore) => {
84
84
  return locale;
85
85
  };
86
86
 
87
+ const dalleConfig = (s: GlobalStore) => s.settings.tool?.dalle || {};
88
+ const isDalleAutoGenerating = (s: GlobalStore) => s.settings.tool?.dalle?.autoGenerate;
89
+
87
90
  export const settingsSelectors = {
88
91
  currentLanguage,
89
92
  currentSettings,
90
93
  currentTTS,
94
+ dalleConfig,
91
95
  defaultAgent,
92
96
  defaultAgentConfig,
93
97
  defaultAgentMeta,
94
98
  exportSettings,
99
+ isDalleAutoGenerating,
95
100
  modelList: modelListSelectors,
96
101
  openAIAPI: openAIAPIKeySelectors,
97
102
  openAIProxyUrl: openAIProxyUrlSelectors,
@@ -1,14 +1,15 @@
1
- import { LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
1
+ import { PluginSchema } from '@lobehub/chat-plugin-sdk';
2
2
 
3
+ import { MetaData } from '@/types/meta';
3
4
  import { LobeTool } from '@/types/tool';
4
5
 
5
6
  const getPluginFormList = (list: LobeTool[], id: string) => list?.find((p) => p.identifier === id);
6
7
 
7
- const getPluginTitle = (meta?: LobeChatPluginManifest['meta']) => meta?.title;
8
- const getPluginDesc = (meta?: LobeChatPluginManifest['meta']) => meta?.description;
8
+ const getPluginTitle = (meta?: MetaData) => meta?.title;
9
+ const getPluginDesc = (meta?: MetaData) => meta?.description;
9
10
 
10
- const getPluginTags = (meta?: LobeChatPluginManifest['meta']) => meta?.tags;
11
- const getPluginAvatar = (meta?: LobeChatPluginManifest['meta']) => meta?.avatar || '🧩';
11
+ const getPluginTags = (meta?: MetaData) => meta?.tags;
12
+ const getPluginAvatar = (meta?: MetaData) => meta?.avatar || '🧩';
12
13
 
13
14
  const isCustomPlugin = (id: string, pluginList: LobeTool[]) =>
14
15
  pluginList.some((i) => i.identifier === id && i.type === 'customPlugin');
@@ -1,11 +1,13 @@
1
+ import { BuiltinToolState, initialBuiltinToolState } from './slices/builtin';
1
2
  import { CustomPluginState, initialCustomPluginState } from './slices/customPlugin';
2
3
  import { PluginState, initialPluginState } from './slices/plugin';
3
4
  import { PluginStoreState, initialPluginStoreState } from './slices/store';
4
5
 
5
- export type ToolStoreState = PluginState & CustomPluginState & PluginStoreState;
6
+ export type ToolStoreState = PluginState & CustomPluginState & PluginStoreState & BuiltinToolState;
6
7
 
7
8
  export const initialState: ToolStoreState = {
8
9
  ...initialPluginState,
9
10
  ...initialCustomPluginState,
10
11
  ...initialPluginStoreState,
12
+ ...initialBuiltinToolState,
11
13
  };
@@ -0,0 +1,5 @@
1
+ export { builtinToolSelectors } from '../slices/builtin/selectors';
2
+ export { customPluginSelectors } from '../slices/customPlugin/selectors';
3
+ export { pluginSelectors } from '../slices/plugin/selectors';
4
+ export { pluginStoreSelectors } from '../slices/store/selectors';
5
+ export { toolSelectors } from './tool';
@@ -0,0 +1,128 @@
1
+ import { LobeChatPluginManifest, LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { initialState } from '../initialState';
5
+ import { ToolStoreState } from '../initialState';
6
+ import { toolSelectors } from './tool';
7
+
8
+ const mockState = {
9
+ ...initialState,
10
+ pluginInstallLoading: {
11
+ 'plugin-1': false,
12
+ 'plugin-2': true,
13
+ },
14
+ installedPlugins: [
15
+ {
16
+ identifier: 'plugin-1',
17
+ type: 'plugin',
18
+ manifest: {
19
+ identifier: 'plugin-1',
20
+ api: [{ name: 'api-1' }],
21
+ type: 'default',
22
+ } as LobeChatPluginManifest,
23
+ settings: { setting1: 'value1' },
24
+ },
25
+ {
26
+ identifier: 'plugin-2',
27
+ manifest: {
28
+ identifier: 'plugin-2',
29
+ api: [{ name: 'api-2' }],
30
+ },
31
+ type: 'plugin',
32
+ },
33
+ {
34
+ identifier: 'plugin-3',
35
+ manifest: {
36
+ identifier: 'plugin-3',
37
+ api: [{ name: 'api-3' }],
38
+ },
39
+ type: 'customPlugin',
40
+ },
41
+ ],
42
+ pluginStoreList: [
43
+ {
44
+ identifier: 'plugin-1',
45
+ author: 'Author 1',
46
+ createdAt: '2021-01-01',
47
+ meta: { avatar: 'avatar-url-1', title: 'Plugin 1' },
48
+ homepage: 'http://homepage-1.com',
49
+ } as LobeChatPluginMeta,
50
+ {
51
+ identifier: 'plugin-2',
52
+ author: 'Author 2',
53
+ createdAt: '2022-02-02',
54
+ meta: { avatar: 'avatar-url-2', title: 'Plugin 2' },
55
+ homepage: 'http://homepage-2.com',
56
+ },
57
+ ],
58
+ } as ToolStoreState;
59
+
60
+ describe('toolSelectors', () => {
61
+ describe('enabledSchema', () => {
62
+ it('enabledSchema should return correct ChatCompletionFunctions array', () => {
63
+ const result = toolSelectors.enabledSchema(['plugin-1', 'plugin-2'])(mockState);
64
+ expect(result).toEqual([{ name: 'plugin-1____api-1' }, { name: 'plugin-2____api-2' }]);
65
+ });
66
+
67
+ it('enabledSchema should return with standalone plugin', () => {
68
+ const result = toolSelectors.enabledSchema(['plugin-4'])({
69
+ ...mockState,
70
+ installedPlugins: [
71
+ ...mockState.installedPlugins,
72
+ {
73
+ identifier: 'plugin-4',
74
+ manifest: {
75
+ identifier: 'plugin-4',
76
+ api: [{ name: 'api-4' }],
77
+ type: 'standalone',
78
+ },
79
+ type: 'plugin',
80
+ },
81
+ ],
82
+ } as ToolStoreState);
83
+ expect(result).toEqual([{ name: 'plugin-4____api-4____standalone' }]);
84
+ });
85
+
86
+ it('enabledSchema should return md5 hash apiName', () => {
87
+ const result = toolSelectors.enabledSchema(['long-long-plugin-with-id'])({
88
+ ...mockState,
89
+ installedPlugins: [
90
+ ...mockState.installedPlugins,
91
+ {
92
+ identifier: 'long-long-plugin-with-id',
93
+ manifest: {
94
+ identifier: 'long-long-plugin-with-id',
95
+ api: [{ name: 'long-long-manifest-long-long-apiName' }],
96
+ },
97
+ type: 'plugin',
98
+ },
99
+ ],
100
+ } as ToolStoreState);
101
+ expect(result).toEqual([
102
+ { name: 'long-long-plugin-with-id____MD5HASH_396eae4c671da3fb642c49ad2b9e8790' },
103
+ ]);
104
+ });
105
+
106
+ it('enabledSchema should return empty', () => {
107
+ const result = toolSelectors.enabledSchema([])(mockState);
108
+ expect(result).toEqual([]);
109
+ });
110
+ });
111
+
112
+ describe('getPluginManifestLoadingStatus', () => {
113
+ it('should return "loading" if the plugin manifest is being loaded', () => {
114
+ const result = toolSelectors.getManifestLoadingStatus('plugin-2')(mockState);
115
+ expect(result).toBe('loading');
116
+ });
117
+
118
+ it('should return "error" if the plugin manifest is not found', () => {
119
+ const result = toolSelectors.getManifestLoadingStatus('non-existing-plugin')(mockState);
120
+ expect(result).toBe('error');
121
+ });
122
+
123
+ it('should return "success" if the plugin manifest is loaded', () => {
124
+ const result = toolSelectors.getManifestLoadingStatus('plugin-1')(mockState);
125
+ expect(result).toBe('success');
126
+ });
127
+ });
128
+ });