@lobehub/chat 0.114.8 → 0.115.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.i18nrc.js +3 -0
  2. package/CHANGELOG.md +50 -0
  3. package/locales/de-DE/common.json +20 -0
  4. package/locales/de-DE/tool.json +8 -0
  5. package/locales/en-US/common.json +20 -0
  6. package/locales/en-US/tool.json +8 -0
  7. package/locales/es-ES/common.json +20 -0
  8. package/locales/es-ES/tool.json +8 -0
  9. package/locales/fr-FR/common.json +20 -0
  10. package/locales/fr-FR/tool.json +8 -0
  11. package/locales/it-IT/chat.json +77 -0
  12. package/locales/it-IT/common.json +113 -0
  13. package/locales/it-IT/empty.json +6 -0
  14. package/locales/it-IT/error.json +53 -0
  15. package/locales/it-IT/market.json +30 -0
  16. package/locales/it-IT/migration.json +45 -0
  17. package/locales/it-IT/plugin.json +166 -0
  18. package/locales/it-IT/setting.json +306 -0
  19. package/locales/it-IT/tool.json +8 -0
  20. package/locales/it-IT/welcome.json +14 -0
  21. package/locales/ja-JP/common.json +20 -0
  22. package/locales/ja-JP/tool.json +8 -0
  23. package/locales/ko-KR/common.json +20 -0
  24. package/locales/ko-KR/tool.json +8 -0
  25. package/locales/nl-NL/chat.json +77 -0
  26. package/locales/nl-NL/common.json +113 -0
  27. package/locales/nl-NL/empty.json +6 -0
  28. package/locales/nl-NL/error.json +53 -0
  29. package/locales/nl-NL/market.json +30 -0
  30. package/locales/nl-NL/migration.json +45 -0
  31. package/locales/nl-NL/plugin.json +166 -0
  32. package/locales/nl-NL/setting.json +306 -0
  33. package/locales/nl-NL/tool.json +8 -0
  34. package/locales/nl-NL/welcome.json +14 -0
  35. package/locales/pl-PL/chat.json +77 -0
  36. package/locales/pl-PL/common.json +113 -0
  37. package/locales/pl-PL/empty.json +6 -0
  38. package/locales/pl-PL/error.json +53 -0
  39. package/locales/pl-PL/market.json +30 -0
  40. package/locales/pl-PL/migration.json +45 -0
  41. package/locales/pl-PL/plugin.json +166 -0
  42. package/locales/pl-PL/setting.json +306 -0
  43. package/locales/pl-PL/tool.json +8 -0
  44. package/locales/pl-PL/welcome.json +14 -0
  45. package/locales/pt-BR/common.json +20 -0
  46. package/locales/pt-BR/tool.json +8 -0
  47. package/locales/ru-RU/common.json +20 -0
  48. package/locales/ru-RU/tool.json +8 -0
  49. package/locales/tr-TR/common.json +20 -0
  50. package/locales/tr-TR/tool.json +8 -0
  51. package/locales/zh-CN/common.json +20 -0
  52. package/locales/zh-CN/plugin.json +2 -2
  53. package/locales/zh-CN/setting.json +1 -1
  54. package/locales/zh-CN/tool.json +8 -0
  55. package/locales/zh-TW/common.json +20 -0
  56. package/locales/zh-TW/tool.json +8 -0
  57. package/package.json +1 -1
  58. package/src/app/api/openai/images/createImageGeneration.ts +26 -0
  59. package/src/app/api/openai/images/route.ts +16 -0
  60. package/src/app/api/proxy/route.ts +3 -1
  61. package/src/app/chat/(desktop)/features/ChatHeader.tsx +56 -56
  62. package/src/app/chat/features/ChatHeader/PluginTag/PluginStatus.tsx +7 -4
  63. package/src/app/chat/features/ChatHeader/PluginTag/index.tsx +3 -3
  64. package/src/app/chat/features/SystemRole/index.tsx +0 -1
  65. package/src/components/GalleyGrid/Grid.tsx +36 -0
  66. package/src/components/GalleyGrid/index.tsx +58 -0
  67. package/src/components/GalleyGrid/style.ts +24 -0
  68. package/src/const/settings.ts +7 -0
  69. package/src/database/schemas/files.ts +1 -0
  70. package/src/database/schemas/message.ts +1 -1
  71. package/src/features/AgentSetting/AgentPlugin/index.tsx +2 -2
  72. package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +2 -1
  73. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +3 -3
  74. package/src/features/ChatInput/ActionBar/Tools/index.tsx +26 -14
  75. package/src/features/Conversation/ChatList/Messages/Function.tsx +1 -1
  76. package/src/features/Conversation/ChatList/Plugins/Inspector/index.tsx +4 -2
  77. package/src/features/Conversation/ChatList/Plugins/Render/BuiltinType/index.tsx +36 -0
  78. package/src/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx +6 -66
  79. package/src/features/Conversation/ChatList/Plugins/Render/Loading.tsx +61 -0
  80. package/src/features/Conversation/ChatList/Plugins/Render/index.tsx +21 -12
  81. package/src/features/Conversation/ChatList/Plugins/Render/useParseContent.ts +15 -0
  82. package/src/locales/default/common.ts +20 -0
  83. package/src/locales/default/index.ts +2 -0
  84. package/src/locales/default/plugin.ts +2 -2
  85. package/src/locales/default/setting.ts +1 -1
  86. package/src/locales/default/tool.ts +9 -0
  87. package/src/locales/options.ts +14 -2
  88. package/src/services/__tests__/chat.test.ts +132 -2
  89. package/src/services/_url.ts +1 -0
  90. package/src/services/chat.ts +3 -5
  91. package/src/services/file.ts +20 -5
  92. package/src/services/imageGeneration.ts +26 -0
  93. package/src/store/chat/actions/enhance.ts +65 -1
  94. package/src/store/chat/actions/plugin.test.ts +9 -9
  95. package/src/store/chat/actions/plugin.ts +38 -6
  96. package/src/store/chat/initialState.ts +3 -0
  97. package/src/store/chat/selectors/enchance.ts +11 -0
  98. package/src/store/chat/selectors/index.ts +1 -0
  99. package/src/store/global/selectors/__snapshots__/settings.test.ts.snap +5 -0
  100. package/src/store/global/selectors/settings.ts +5 -0
  101. package/src/store/tool/helpers.ts +6 -5
  102. package/src/store/tool/initialState.ts +3 -1
  103. package/src/store/tool/selectors/index.ts +5 -0
  104. package/src/store/tool/selectors/tool.test.ts +128 -0
  105. package/src/store/tool/selectors/tool.ts +125 -0
  106. package/src/store/tool/slices/builtin/action.ts +54 -0
  107. package/src/store/tool/slices/builtin/index.ts +2 -0
  108. package/src/store/tool/slices/builtin/initialState.ts +13 -0
  109. package/src/store/tool/slices/builtin/selectors.test.ts +70 -0
  110. package/src/store/tool/slices/builtin/selectors.ts +20 -0
  111. package/src/store/tool/slices/customPlugin/action.ts +0 -3
  112. package/src/store/tool/slices/plugin/selectors.test.ts +0 -69
  113. package/src/store/tool/slices/plugin/selectors.ts +1 -84
  114. package/src/store/tool/store.ts +8 -2
  115. package/src/tools/dalle/Render/EditMode.tsx +66 -0
  116. package/src/tools/dalle/Render/Item.tsx +97 -0
  117. package/src/tools/dalle/Render/ToolBar.tsx +54 -0
  118. package/src/tools/dalle/Render/index.tsx +21 -0
  119. package/src/tools/dalle/index.ts +81 -0
  120. package/src/tools/index.ts +11 -0
  121. package/src/tools/renders.ts +8 -0
  122. package/src/types/chatMessage.ts +1 -1
  123. package/src/types/openai/image.ts +34 -0
  124. package/src/types/settings.ts +6 -0
  125. package/src/types/tool/builtin.ts +36 -0
  126. package/src/types/tool/dalle.ts +12 -0
  127. package/src/types/tool/index.ts +5 -1
  128. package/src/types/tool/tool.ts +9 -0
  129. package/src/store/tool/selectors.ts +0 -3
@@ -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
+ });
@@ -0,0 +1,125 @@
1
+ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
2
+ import { uniqBy } from 'lodash-es';
3
+ import { Md5 } from 'ts-md5';
4
+
5
+ import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
6
+ import { MetaData } from '@/types/meta';
7
+ import { ChatCompletionFunctions } from '@/types/openai/chat';
8
+ import { LobeToolMeta } from '@/types/tool/tool';
9
+
10
+ import { pluginHelpers } from '../helpers';
11
+ import { ToolStoreState } from '../initialState';
12
+ import { builtinToolSelectors } from '../slices/builtin/selectors';
13
+ import { pluginSelectors } from '../slices/plugin/selectors';
14
+
15
+ const getAPIName = (identifier: string, name: string, type?: string) => {
16
+ const pluginType = type && type !== 'default' ? `${PLUGIN_SCHEMA_SEPARATOR + type}` : '';
17
+
18
+ // 将插件的 identifier 作为前缀,避免重复
19
+ let apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + name + pluginType;
20
+
21
+ // OpenAI GPT function_call name can't be longer than 64 characters
22
+ // So we need to use md5 to shorten the name
23
+ // and then find the correct apiName in response by md5
24
+ if (apiName.length >= 64) {
25
+ const md5Content = PLUGIN_SCHEMA_API_MD5_PREFIX + Md5.hashStr(name).toString();
26
+
27
+ apiName = identifier + PLUGIN_SCHEMA_SEPARATOR + md5Content + pluginType;
28
+ }
29
+
30
+ return apiName;
31
+ };
32
+
33
+ const enabledSchema =
34
+ (tools: string[] = []) =>
35
+ (s: ToolStoreState): ChatCompletionFunctions[] => {
36
+ const list = pluginSelectors
37
+ .installedPluginManifestList(s)
38
+ .concat(s.builtinTools.map((b) => b.manifest as LobeChatPluginManifest))
39
+ // 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件
40
+ .filter((m) => tools.includes(m?.identifier))
41
+ .flatMap((manifest) =>
42
+ manifest.api.map((m) => ({
43
+ ...m,
44
+ name: getAPIName(manifest.identifier, m.name, manifest.type),
45
+ })),
46
+ );
47
+
48
+ return uniqBy(list, 'name');
49
+ };
50
+
51
+ const enabledSystemRoles =
52
+ (tools: string[] = []) =>
53
+ (s: ToolStoreState) => {
54
+ const toolsSystemRole = pluginSelectors
55
+ .installedPluginManifestList(s)
56
+ .concat(s.builtinTools.map((b) => b.manifest as LobeChatPluginManifest))
57
+ // 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件
58
+ .filter((m) => tools.includes(m?.identifier))
59
+ .map((manifest) => {
60
+ if (!manifest) return '';
61
+
62
+ const meta = manifest.meta || {};
63
+
64
+ const title = pluginHelpers.getPluginTitle(meta) || manifest.identifier;
65
+ const systemRole = manifest.systemRole || pluginHelpers.getPluginDesc(meta);
66
+
67
+ const methods = manifest.api
68
+ .map((m) =>
69
+ [`#### ${getAPIName(manifest.identifier, m.name, manifest.type)}`, m.description].join(
70
+ '\n\n',
71
+ ),
72
+ )
73
+ .join('\n\n');
74
+
75
+ return [`### ${title}`, systemRole, 'The APIs you can use:', methods].join('\n\n');
76
+ })
77
+ .filter(Boolean);
78
+
79
+ if (toolsSystemRole.length > 0) {
80
+ return ['## Tools', 'You can use these tools below:', ...toolsSystemRole]
81
+ .filter(Boolean)
82
+ .join('\n\n');
83
+ }
84
+
85
+ return '';
86
+ };
87
+
88
+ const metaList = (s: ToolStoreState): LobeToolMeta[] => {
89
+ const pluginList = pluginSelectors.installedPluginMetaList(s) as LobeToolMeta[];
90
+
91
+ return builtinToolSelectors.metaList(s).concat(pluginList);
92
+ };
93
+
94
+ const getMetaById =
95
+ (id: string) =>
96
+ (s: ToolStoreState): MetaData | undefined =>
97
+ metaList(s).find((m) => m.identifier === id)?.meta;
98
+
99
+ const getManifestById =
100
+ (id: string) =>
101
+ (s: ToolStoreState): LobeChatPluginManifest | undefined =>
102
+ pluginSelectors
103
+ .installedPluginManifestList(s)
104
+ .concat(s.builtinTools.map((b) => b.manifest as LobeChatPluginManifest))
105
+ .find((i) => i.identifier === id);
106
+
107
+ // 获取插件 manifest 加载状态
108
+ const getManifestLoadingStatus = (id: string) => (s: ToolStoreState) => {
109
+ const manifest = getManifestById(id)(s);
110
+
111
+ if (s.pluginInstallLoading[id]) return 'loading';
112
+
113
+ if (!manifest) return 'error';
114
+
115
+ if (!!manifest) return 'success';
116
+ };
117
+
118
+ export const toolSelectors = {
119
+ enabledSchema,
120
+ enabledSystemRoles,
121
+ getManifestById,
122
+ getManifestLoadingStatus,
123
+ getMetaById,
124
+ metaList,
125
+ };
@@ -0,0 +1,54 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { OpenAIImagePayload } from '@/types/openai/image';
4
+ import { DallEImageItem } from '@/types/tool/dalle';
5
+ import { setNamespace } from '@/utils/storeDebug';
6
+
7
+ import { ToolStore } from '../../store';
8
+
9
+ const n = setNamespace('builtinTool');
10
+
11
+ interface Text2ImageParams extends Pick<OpenAIImagePayload, 'quality' | 'style' | 'size'> {
12
+ prompts: string[];
13
+ }
14
+
15
+ /**
16
+ * 代理行为接口
17
+ */
18
+ export interface BuiltinToolAction {
19
+ invokeBuiltinTool: (key: string, params: any) => Promise<string | undefined>;
20
+ text2image: (params: Text2ImageParams, messageId: string) => DallEImageItem[];
21
+ toggleBuiltinToolLoading: (key: string, value: boolean) => void;
22
+ }
23
+
24
+ export const createBuiltinToolSlice: StateCreator<
25
+ ToolStore,
26
+ [['zustand/devtools', never]],
27
+ [],
28
+ BuiltinToolAction
29
+ > = (set, get) => ({
30
+ invokeBuiltinTool: async (key, params) => {
31
+ const { builtinToolLoading, toggleBuiltinToolLoading } = get();
32
+
33
+ if (builtinToolLoading[key]) return;
34
+
35
+ toggleBuiltinToolLoading(key, true);
36
+
37
+ const { [key as keyof BuiltinToolAction]: action } = get();
38
+
39
+ if (!action) return;
40
+
41
+ // @ts-ignore
42
+ const result = await action(params);
43
+
44
+ toggleBuiltinToolLoading(key, false);
45
+
46
+ return JSON.stringify(result);
47
+ },
48
+ text2image: ({ prompts, size = '1024x1024' as const, quality = 'standard', style = 'vivid' }) =>
49
+ prompts.map((p) => ({ prompt: p, quality, size, style })),
50
+
51
+ toggleBuiltinToolLoading: (key, value) => {
52
+ set({ builtinToolLoading: { [key]: value } }, false, n('toggleBuiltinToolLoading'));
53
+ },
54
+ });
@@ -0,0 +1,2 @@
1
+ export * from './action';
2
+ export * from './initialState';