@lobehub/chat 1.63.3 → 1.64.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 (148) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/models.json +25 -16
  4. package/locales/ar/plugin.json +16 -0
  5. package/locales/ar/portal.json +0 -5
  6. package/locales/ar/tool.json +18 -0
  7. package/locales/bg-BG/models.json +25 -16
  8. package/locales/bg-BG/plugin.json +16 -0
  9. package/locales/bg-BG/portal.json +0 -5
  10. package/locales/bg-BG/tool.json +18 -0
  11. package/locales/de-DE/models.json +25 -16
  12. package/locales/de-DE/plugin.json +16 -0
  13. package/locales/de-DE/portal.json +0 -5
  14. package/locales/de-DE/tool.json +18 -0
  15. package/locales/en-US/models.json +24 -15
  16. package/locales/en-US/plugin.json +16 -0
  17. package/locales/en-US/portal.json +0 -5
  18. package/locales/en-US/tool.json +18 -0
  19. package/locales/es-ES/models.json +25 -16
  20. package/locales/es-ES/plugin.json +16 -0
  21. package/locales/es-ES/portal.json +0 -5
  22. package/locales/es-ES/tool.json +18 -0
  23. package/locales/fa-IR/models.json +25 -16
  24. package/locales/fa-IR/plugin.json +16 -0
  25. package/locales/fa-IR/portal.json +0 -5
  26. package/locales/fa-IR/tool.json +18 -0
  27. package/locales/fr-FR/models.json +25 -16
  28. package/locales/fr-FR/plugin.json +16 -0
  29. package/locales/fr-FR/portal.json +0 -5
  30. package/locales/fr-FR/tool.json +18 -0
  31. package/locales/it-IT/models.json +25 -16
  32. package/locales/it-IT/plugin.json +16 -0
  33. package/locales/it-IT/portal.json +0 -5
  34. package/locales/it-IT/tool.json +18 -0
  35. package/locales/ja-JP/models.json +24 -15
  36. package/locales/ja-JP/plugin.json +16 -0
  37. package/locales/ja-JP/portal.json +0 -5
  38. package/locales/ja-JP/tool.json +18 -0
  39. package/locales/ko-KR/models.json +25 -16
  40. package/locales/ko-KR/plugin.json +16 -0
  41. package/locales/ko-KR/portal.json +0 -5
  42. package/locales/ko-KR/tool.json +18 -0
  43. package/locales/nl-NL/models.json +25 -16
  44. package/locales/nl-NL/plugin.json +16 -0
  45. package/locales/nl-NL/portal.json +0 -5
  46. package/locales/nl-NL/tool.json +18 -0
  47. package/locales/pl-PL/models.json +25 -16
  48. package/locales/pl-PL/plugin.json +16 -0
  49. package/locales/pl-PL/portal.json +0 -5
  50. package/locales/pl-PL/tool.json +18 -0
  51. package/locales/pt-BR/models.json +24 -15
  52. package/locales/pt-BR/plugin.json +16 -0
  53. package/locales/pt-BR/portal.json +0 -5
  54. package/locales/pt-BR/tool.json +18 -0
  55. package/locales/ru-RU/models.json +25 -16
  56. package/locales/ru-RU/plugin.json +16 -0
  57. package/locales/ru-RU/portal.json +0 -5
  58. package/locales/ru-RU/tool.json +18 -0
  59. package/locales/tr-TR/models.json +25 -16
  60. package/locales/tr-TR/plugin.json +16 -0
  61. package/locales/tr-TR/portal.json +0 -5
  62. package/locales/tr-TR/tool.json +18 -0
  63. package/locales/vi-VN/models.json +24 -15
  64. package/locales/vi-VN/plugin.json +16 -0
  65. package/locales/vi-VN/portal.json +0 -5
  66. package/locales/vi-VN/tool.json +18 -0
  67. package/locales/zh-CN/models.json +30 -21
  68. package/locales/zh-CN/plugin.json +16 -0
  69. package/locales/zh-CN/portal.json +1 -6
  70. package/locales/zh-CN/tool.json +19 -1
  71. package/locales/zh-TW/models.json +23 -14
  72. package/locales/zh-TW/plugin.json +16 -0
  73. package/locales/zh-TW/portal.json +0 -5
  74. package/locales/zh-TW/tool.json +18 -0
  75. package/package.json +1 -1
  76. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +1 -0
  77. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/SearchTags.tsx +17 -0
  78. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Tags.tsx +8 -2
  79. package/src/config/tools.ts +16 -0
  80. package/src/features/ChatInput/ActionBar/Search/index.tsx +6 -15
  81. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/ToolTitle.tsx +76 -0
  82. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/index.tsx +8 -21
  83. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +62 -50
  84. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +11 -1
  85. package/src/features/PluginsUI/Render/index.tsx +3 -0
  86. package/src/features/Portal/Plugins/Body/index.tsx +3 -7
  87. package/src/features/Portal/Plugins/Header.tsx +14 -2
  88. package/src/hooks/useAgentEnableSearch.ts +27 -0
  89. package/src/libs/trpc/client/index.ts +1 -0
  90. package/src/libs/trpc/client/tools.ts +20 -0
  91. package/src/locales/default/plugin.ts +16 -0
  92. package/src/locales/default/portal.ts +0 -5
  93. package/src/locales/default/tool.ts +18 -0
  94. package/src/server/modules/SearXNG.ts +33 -0
  95. package/src/server/routers/lambda/message.ts +11 -0
  96. package/src/server/routers/tools/__tests__/fixtures/searXNG.ts +668 -0
  97. package/src/server/routers/tools/__tests__/search.test.ts +47 -0
  98. package/src/server/routers/tools/index.ts +3 -0
  99. package/src/server/routers/tools/search.ts +38 -0
  100. package/src/services/__tests__/__snapshots__/chat.test.ts.snap +1 -0
  101. package/src/services/_auth.ts +4 -4
  102. package/src/services/chat.ts +31 -10
  103. package/src/services/message/_deprecated.ts +4 -0
  104. package/src/services/message/client.ts +4 -0
  105. package/src/services/message/server.ts +5 -5
  106. package/src/services/message/type.ts +2 -0
  107. package/src/services/search.ts +9 -0
  108. package/src/store/aiInfra/slices/aiModel/selectors.ts +12 -5
  109. package/src/store/chat/slices/builtinTool/action.ts +121 -0
  110. package/src/store/chat/slices/builtinTool/initialState.ts +2 -0
  111. package/src/store/chat/slices/builtinTool/selectors.ts +3 -0
  112. package/src/store/chat/slices/message/action.ts +11 -0
  113. package/src/store/chat/slices/plugin/action.test.ts +2 -2
  114. package/src/store/chat/slices/plugin/action.ts +2 -2
  115. package/src/store/tool/selectors/tool.ts +5 -12
  116. package/src/store/tool/slices/builtin/selectors.ts +1 -1
  117. package/src/store/user/slices/modelList/action.ts +6 -0
  118. package/src/store/user/slices/modelList/selectors/keyVaults.ts +1 -0
  119. package/src/tools/index.ts +7 -0
  120. package/src/tools/portals.ts +6 -1
  121. package/src/tools/renders.ts +3 -0
  122. package/src/{features/Portal/Plugins → tools/web-browsing/Portal}/Footer.tsx +13 -10
  123. package/src/tools/web-browsing/Portal/ResultList/SearchItem/CategoryAvatar.tsx +70 -0
  124. package/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx +38 -0
  125. package/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx +135 -0
  126. package/src/tools/web-browsing/Portal/ResultList/SearchItem/index.tsx +91 -0
  127. package/src/tools/web-browsing/Portal/ResultList/index.tsx +21 -0
  128. package/src/tools/web-browsing/Portal/index.tsx +65 -0
  129. package/src/tools/web-browsing/Render/ConfigForm/Form.tsx +110 -0
  130. package/src/tools/web-browsing/Render/ConfigForm/SearchXNGIcon.tsx +20 -0
  131. package/src/tools/web-browsing/Render/ConfigForm/index.tsx +67 -0
  132. package/src/tools/web-browsing/Render/ConfigForm/style.tsx +63 -0
  133. package/src/tools/web-browsing/Render/SearchQuery/SearchView.tsx +88 -0
  134. package/src/tools/web-browsing/Render/SearchQuery/index.tsx +61 -0
  135. package/src/tools/web-browsing/Render/SearchResult/SearchResultItem.tsx +72 -0
  136. package/src/tools/web-browsing/Render/SearchResult/ShowMore.tsx +68 -0
  137. package/src/tools/web-browsing/Render/SearchResult/index.tsx +105 -0
  138. package/src/tools/web-browsing/Render/index.tsx +57 -0
  139. package/src/tools/web-browsing/components/EngineAvatar.tsx +32 -0
  140. package/src/tools/web-browsing/components/SearchBar.tsx +134 -0
  141. package/src/tools/web-browsing/const.ts +11 -0
  142. package/src/tools/web-browsing/index.ts +102 -0
  143. package/src/types/message/chat.ts +1 -0
  144. package/src/types/message/tools.ts +10 -0
  145. package/src/types/tool/builtin.ts +2 -0
  146. package/src/types/tool/search.ts +38 -0
  147. package/src/types/user/settings/keyVaults.ts +8 -1
  148. package/src/utils/toolManifest.ts +20 -0
@@ -0,0 +1,47 @@
1
+ // @vitest-environment node
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { toolsEnv } from '@/config/tools';
5
+ import { isServerMode } from '@/const/version';
6
+ /**
7
+ * This file contains the root router of your tRPC-backend
8
+ */
9
+ import { createCallerFactory } from '@/libs/trpc';
10
+ import { AuthContext, createContextInner } from '@/server/context';
11
+ import { SearXNGClient } from '@/server/modules/SearXNG';
12
+
13
+ import { searchRouter } from '../search';
14
+ import { hetongxue } from './fixtures/searXNG';
15
+
16
+ vi.mock('@/config/tools', () => ({
17
+ toolsEnv: {
18
+ SEARXNG_URL: 'https://demo.com',
19
+ },
20
+ }));
21
+
22
+ vi.mock('@/const/version', () => ({
23
+ isServerMode: true,
24
+ }));
25
+
26
+ const createCaller = createCallerFactory(searchRouter);
27
+ let ctx: AuthContext;
28
+ let router: ReturnType<typeof createCaller>;
29
+
30
+ beforeEach(async () => {
31
+ vi.resetAllMocks();
32
+ ctx = await createContextInner({ userId: 'mock' });
33
+ router = createCaller(ctx);
34
+ });
35
+
36
+ describe('searchRouter', () => {
37
+ describe('search', () => {
38
+ it('搜索结果超过10个', async () => {
39
+ vi.spyOn(SearXNGClient.prototype, 'search').mockResolvedValueOnce(hetongxue);
40
+
41
+ const results = await router.query({ query: '何同学' });
42
+
43
+ // Assert
44
+ expect(results.results.length).toEqual(43);
45
+ });
46
+ });
47
+ });
@@ -1,7 +1,10 @@
1
1
  import { publicProcedure, router } from '@/libs/trpc';
2
2
 
3
+ import { searchRouter } from './search';
4
+
3
5
  export const toolsRouter = router({
4
6
  healthcheck: publicProcedure.query(() => "i'm live!"),
7
+ search: searchRouter,
5
8
  });
6
9
 
7
10
  export type ToolsRouter = typeof toolsRouter;
@@ -0,0 +1,38 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { z } from 'zod';
3
+
4
+ import { toolsEnv } from '@/config/tools';
5
+ import { isServerMode } from '@/const/version';
6
+ import { authedProcedure, passwordProcedure, router } from '@/libs/trpc';
7
+ import { SearXNGClient } from '@/server/modules/SearXNG';
8
+ import { SEARCH_SEARXNG_NOT_CONFIG } from '@/types/tool/search';
9
+
10
+ const searchProcedure = isServerMode ? authedProcedure : passwordProcedure;
11
+
12
+ export const searchRouter = router({
13
+ query: searchProcedure
14
+ .input(
15
+ z.object({
16
+ query: z.string(),
17
+ searchEngine: z.array(z.string()).optional(),
18
+ }),
19
+ )
20
+ .query(async ({ input }) => {
21
+ if (!toolsEnv.SEARXNG_URL) {
22
+ throw new TRPCError({ code: 'NOT_IMPLEMENTED', message: SEARCH_SEARXNG_NOT_CONFIG });
23
+ }
24
+
25
+ const client = new SearXNGClient(toolsEnv.SEARXNG_URL);
26
+
27
+ try {
28
+ return await client.search(input.query, input.searchEngine);
29
+ } catch (e) {
30
+ console.error(e);
31
+
32
+ throw new TRPCError({
33
+ code: 'SERVICE_UNAVAILABLE',
34
+ message: (e as Error).message,
35
+ });
36
+ }
37
+ }),
38
+ });
@@ -2,6 +2,7 @@
2
2
 
3
3
  exports[`ChatService > createAssistantMessage > with tools messages > work with dalle3 1`] = `
4
4
  {
5
+ "enabledSearch": undefined,
5
6
  "messages": [
6
7
  {
7
8
  "content": "<plugins description="The plugins you can use below">
@@ -48,10 +48,10 @@ export const getProviderAuthPayload = (
48
48
  case ModelProvider.Azure: {
49
49
  return {
50
50
  apiKey: keyVaults.apiKey,
51
-
51
+
52
52
  apiVersion: keyVaults.apiVersion,
53
53
  /** @deprecated */
54
- azureApiVersion: keyVaults.apiVersion,
54
+ azureApiVersion: keyVaults.apiVersion,
55
55
  baseURL: keyVaults.baseURL || keyVaults.endpoint,
56
56
  };
57
57
  }
@@ -63,10 +63,10 @@ azureApiVersion: keyVaults.apiVersion,
63
63
  case ModelProvider.Cloudflare: {
64
64
  return {
65
65
  apiKey: keyVaults?.apiKey,
66
-
66
+
67
67
  baseURLOrAccountID: keyVaults?.baseURLOrAccountID,
68
68
  /** @deprecated */
69
- cloudflareBaseURLOrAccountID: keyVaults?.baseURLOrAccountID,
69
+ cloudflareBaseURLOrAccountID: keyVaults?.baseURLOrAccountID,
70
70
  };
71
71
  }
72
72
 
@@ -30,6 +30,7 @@ import {
30
30
  preferenceSelectors,
31
31
  userProfileSelectors,
32
32
  } from '@/store/user/selectors';
33
+ import { WebBrowsingManifest } from '@/tools/web-browsing';
33
34
  import { ChatErrorType } from '@/types/fetch';
34
35
  import { ChatMessage, MessageToolCall } from '@/types/message';
35
36
  import type { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
@@ -168,6 +169,25 @@ class ChatService {
168
169
  },
169
170
  params,
170
171
  );
172
+
173
+ // =================== 0. process search =================== //
174
+ const chatConfig = getAgentChatConfig();
175
+
176
+ const enabledSearch = chatConfig.searchMode !== 'off';
177
+ const isModelHasBuiltinSearch = aiModelSelectors.isModelHasBuiltinSearch(
178
+ payload.model,
179
+ payload.provider!,
180
+ )(useAiInfraStore.getState());
181
+
182
+ const useApplicationBuiltinSearchTool =
183
+ enabledSearch && !(isModelHasBuiltinSearch && chatConfig.useModelBuiltinSearch);
184
+
185
+ const pluginIds = [...(enabledPlugins || [])];
186
+
187
+ if (useApplicationBuiltinSearchTool) {
188
+ pluginIds.push(WebBrowsingManifest.identifier);
189
+ }
190
+
171
191
  // ============ 1. preprocess messages ============ //
172
192
 
173
193
  const oaiMessages = this.processMessages(
@@ -175,14 +195,14 @@ class ChatService {
175
195
  messages,
176
196
  model: payload.model,
177
197
  provider: payload.provider!,
178
- tools: enabledPlugins,
198
+ tools: pluginIds,
179
199
  },
180
200
  options,
181
201
  );
182
202
 
183
203
  // ============ 2. preprocess tools ============ //
184
204
 
185
- const filterTools = toolSelectors.enabledSchema(enabledPlugins)(useToolStore.getState());
205
+ let filterTools = toolSelectors.enabledSchema(pluginIds)(useToolStore.getState());
186
206
 
187
207
  // check this model can use function call
188
208
  const canUseFC = isCanUseFC(payload.model, payload.provider!);
@@ -194,7 +214,15 @@ class ChatService {
194
214
 
195
215
  const tools = shouldUseTools ? filterTools : undefined;
196
216
 
197
- return this.getChatCompletion({ ...params, messages: oaiMessages, tools }, options);
217
+ return this.getChatCompletion(
218
+ {
219
+ ...params,
220
+ enabledSearch: enabledSearch && isModelHasBuiltinSearch ? true : undefined,
221
+ messages: oaiMessages,
222
+ tools,
223
+ },
224
+ options,
225
+ );
198
226
  };
199
227
 
200
228
  createAssistantMessageStream = async ({
@@ -241,13 +269,6 @@ class ChatService {
241
269
  model = findDeploymentName(model, provider);
242
270
  }
243
271
 
244
- // =================== process search =================== //
245
- // ===================================================== //
246
- const chatConfig = getAgentChatConfig();
247
- if (chatConfig.searchMode !== 'off') {
248
- res.enabledSearch = true;
249
- }
250
-
251
272
  const payload = merge(
252
273
  { model: DEFAULT_AGENT_CONFIG.model, stream: true, ...DEFAULT_AGENT_CONFIG.params },
253
274
  { ...res, model },
@@ -139,4 +139,8 @@ export class ClientService implements IMessageService {
139
139
  const number = await this.countMessages();
140
140
  return number >= 4;
141
141
  }
142
+
143
+ async updateMessagePluginError() {
144
+ throw new Error('Method not implemented.');
145
+ }
142
146
  }
@@ -92,6 +92,10 @@ export class ClientService extends BaseClientService implements IMessageService
92
92
  return this.messageModel.updatePluginState(id, value);
93
93
  };
94
94
 
95
+ updateMessagePluginError: IMessageService['updateMessagePluginError'] = async (id, value) => {
96
+ return this.messageModel.updateMessagePlugin(id, { error: value });
97
+ };
98
+
95
99
  updateMessagePluginArguments: IMessageService['updateMessagePluginArguments'] = async (
96
100
  id,
97
101
  value,
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
2
  import { INBOX_SESSION_ID } from '@/const/session';
3
3
  import { lambdaClient } from '@/libs/trpc/client';
4
- import { ChatMessage, ChatMessageError, ChatTranslate } from '@/types/message';
4
+ import { ChatMessage, ChatTranslate } from '@/types/message';
5
5
 
6
6
  import { IMessageService } from './type';
7
7
 
@@ -56,10 +56,6 @@ export class ServerService implements IMessageService {
56
56
  return lambdaClient.message.update.mutate({ id, value: { error } });
57
57
  };
58
58
 
59
- updateMessagePluginError = async (id: string, error: ChatMessageError): Promise<any> => {
60
- return lambdaClient.message.update.mutate({ id, value: { pluginError: error } });
61
- };
62
-
63
59
  updateMessagePluginArguments: IMessageService['updateMessagePluginArguments'] = async (
64
60
  id,
65
61
  value,
@@ -84,6 +80,10 @@ export class ServerService implements IMessageService {
84
80
  return lambdaClient.message.updatePluginState.mutate({ id, value });
85
81
  };
86
82
 
83
+ updateMessagePluginError: IMessageService['updateMessagePluginError'] = async (id, error) => {
84
+ return lambdaClient.message.updatePluginError.mutate({ id, value: error as any });
85
+ };
86
+
87
87
  removeMessage: IMessageService['removeMessage'] = async (id) => {
88
88
  return lambdaClient.message.removeMessage.mutate({ id });
89
89
  };
@@ -3,6 +3,7 @@ import type { HeatmapsProps } from '@lobehub/charts';
3
3
  import {
4
4
  ChatMessage,
5
5
  ChatMessageError,
6
+ ChatMessagePluginError,
6
7
  ChatTTS,
7
8
  ChatTranslate,
8
9
  CreateMessageParams,
@@ -36,6 +37,7 @@ export interface IMessageService {
36
37
  updateMessageTTS(id: string, tts: Partial<ChatTTS> | false): Promise<any>;
37
38
  updateMessageTranslate(id: string, translate: Partial<ChatTranslate> | false): Promise<any>;
38
39
  updateMessagePluginState(id: string, value: Record<string, any>): Promise<any>;
40
+ updateMessagePluginError(id: string, value: ChatMessagePluginError | null): Promise<any>;
39
41
  updateMessagePluginArguments(id: string, value: string | Record<string, any>): Promise<any>;
40
42
  removeMessage(id: string): Promise<any>;
41
43
  removeMessages(ids: string[]): Promise<any>;
@@ -0,0 +1,9 @@
1
+ import { toolsClient } from '@/libs/trpc/client';
2
+
3
+ class SearchService {
4
+ search(query: string, searchEngine?: string[]) {
5
+ return toolsClient.search.query.query({ query, searchEngine });
6
+ }
7
+ }
8
+
9
+ export const searchService = new SearchService();
@@ -82,20 +82,26 @@ const isModelHasExtendControls = (id: string, provider: string) => (s: AIProvide
82
82
  return !!controls && controls.length > 0;
83
83
  };
84
84
 
85
- const isModelHasBuiltinSearch = (id: string, provider: string) => (s: AIProviderStoreState) => {
85
+ const modelBuiltinSearchImpl = (id: string, provider: string) => (s: AIProviderStoreState) => {
86
86
  const model = getEnabledModelById(id, provider)(s);
87
87
 
88
- return !!model?.settings?.searchImpl;
88
+ return model?.settings?.searchImpl;
89
+ };
90
+
91
+ const isModelHasBuiltinSearch = (id: string, provider: string) => (s: AIProviderStoreState) => {
92
+ const searchImpl = modelBuiltinSearchImpl(id, provider)(s);
93
+
94
+ return !!searchImpl;
89
95
  };
90
96
 
91
97
  const isModelHasBuiltinSearchConfig =
92
98
  (id: string, provider: string) => (s: AIProviderStoreState) => {
93
- const model = getEnabledModelById(id, provider)(s);
99
+ const searchImpl = modelBuiltinSearchImpl(id, provider)(s);
94
100
 
95
101
  return (
96
- !!model?.settings?.searchImpl &&
102
+ !!searchImpl &&
97
103
  [ModelSearchImplement.Tool, ModelSearchImplement.Params].includes(
98
- model?.settings?.searchImpl as ModelSearchImplement,
104
+ searchImpl as ModelSearchImplement,
99
105
  )
100
106
  );
101
107
  };
@@ -118,6 +124,7 @@ export const aiModelSelectors = {
118
124
  isModelSupportReasoning,
119
125
  isModelSupportToolUse,
120
126
  isModelSupportVision,
127
+ modelBuiltinSearchImpl,
121
128
  modelContextWindowTokens,
122
129
  modelExtendControls,
123
130
  totalAiProviderModelList,
@@ -5,13 +5,22 @@ import { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { useClientDataSWR } from '@/libs/swr';
7
7
  import { fileService } from '@/services/file';
8
+ import { searchService } from '@/services/search';
8
9
  import { imageGenerationService } from '@/services/textToImage';
9
10
  import { uploadService } from '@/services/upload';
10
11
  import { chatSelectors } from '@/store/chat/selectors';
11
12
  import { ChatStore } from '@/store/chat/store';
12
13
  import { useFileStore } from '@/store/file';
14
+ import { CreateMessageParams } from '@/types/message';
13
15
  import { DallEImageItem } from '@/types/tool/dalle';
16
+ import {
17
+ SEARCH_SEARXNG_NOT_CONFIG,
18
+ SearchContent,
19
+ SearchQuery,
20
+ SearchResponse,
21
+ } from '@/types/tool/search';
14
22
  import { setNamespace } from '@/utils/storeDebug';
23
+ import { nanoid } from '@/utils/uuid';
15
24
 
16
25
  const n = setNamespace('tool');
17
26
 
@@ -21,8 +30,25 @@ const SWR_FETCH_KEY = 'FetchImageItem';
21
30
  */
22
31
  export interface ChatBuiltinToolAction {
23
32
  generateImageFromPrompts: (items: DallEImageItem[], id: string) => Promise<void>;
33
+ /**
34
+ * 重新发起搜索
35
+ * @description 会更新插件的 arguments 参数,然后再次搜索
36
+ */
37
+ reSearchWithSearXNG: (
38
+ id: string,
39
+ data: SearchQuery,
40
+ options?: { aiSummary: boolean },
41
+ ) => Promise<void>;
42
+ saveSearXNGSearchResult: (id: string) => Promise<void>;
43
+ searchWithSearXNG: (
44
+ id: string,
45
+ data: SearchQuery,
46
+ aiSummary?: boolean,
47
+ ) => Promise<void | boolean>;
24
48
  text2image: (id: string, data: DallEImageItem[]) => Promise<void>;
49
+
25
50
  toggleDallEImageLoading: (key: string, value: boolean) => void;
51
+ toggleSearchLoading: (id: string, loading: boolean) => void;
26
52
  updateImageItem: (id: string, updater: (data: DallEImageItem[]) => void) => Promise<void>;
27
53
  useFetchDalleImageItem: (id: string) => SWRResponse;
28
54
  }
@@ -82,12 +108,101 @@ export const chatToolSlice: StateCreator<
82
108
  });
83
109
  });
84
110
  },
111
+ reSearchWithSearXNG: async (id, data, options) => {
112
+ get().toggleSearchLoading(id, true);
113
+ await get().updatePluginArguments(id, data);
114
+
115
+ await get().searchWithSearXNG(id, data, options?.aiSummary);
116
+ },
117
+ saveSearXNGSearchResult: async (id) => {
118
+ const message = chatSelectors.getMessageById(id)(get());
119
+ if (!message || !message.plugin) return;
120
+
121
+ const { internal_addToolToAssistantMessage, internal_createMessage, openToolUI } = get();
122
+ // 1. 创建一个新的 tool call message
123
+ const newToolCallId = `tool_call_${nanoid()}`;
124
+
125
+ const toolMessage: CreateMessageParams = {
126
+ content: message.content,
127
+ id: undefined,
128
+ parentId: message.parentId,
129
+ plugin: message.plugin,
130
+ pluginState: message.pluginState,
131
+ role: 'tool',
132
+ sessionId: get().activeId,
133
+ tool_call_id: newToolCallId,
134
+ topicId: get().activeTopicId,
135
+ };
136
+
137
+ const addToolItem = async () => {
138
+ if (!message.parentId || !message.plugin) return;
139
+
140
+ await internal_addToolToAssistantMessage(message.parentId, {
141
+ id: newToolCallId,
142
+ ...message.plugin,
143
+ });
144
+ };
145
+
146
+ const [newMessageId] = await Promise.all([
147
+ // 1. 添加 tool message
148
+ internal_createMessage(toolMessage),
149
+ // 2. 将这条 tool call message 插入到 ai 消息的 tools 中
150
+ addToolItem(),
151
+ ]);
152
+
153
+ // 将新创建的 tool message 激活
154
+ openToolUI(newMessageId, message.plugin.identifier);
155
+ },
156
+ searchWithSearXNG: async (id, params, aiSummary = true) => {
157
+ get().toggleSearchLoading(id, true);
158
+ let data: SearchResponse | undefined;
159
+ try {
160
+ data = await searchService.search(params.query, params.searchEngines);
161
+ await get().updatePluginState(id, data);
162
+ } catch (e) {
163
+ if ((e as Error).message === SEARCH_SEARXNG_NOT_CONFIG) {
164
+ await get().internal_updateMessagePluginError(id, {
165
+ body: {
166
+ provider: 'searxng',
167
+ },
168
+ message: 'SearXNG is not configured',
169
+ type: 'PluginSettingsInvalid',
170
+ });
171
+ } else {
172
+ await get().internal_updateMessagePluginError(id, {
173
+ body: e,
174
+ message: (e as Error).message,
175
+ type: 'PluginServerError',
176
+ });
177
+ }
178
+ }
179
+
180
+ get().toggleSearchLoading(id, false);
181
+
182
+ if (!data) return;
183
+
184
+ // 只取前 5 个结果作为上下文
185
+ const searchContent: SearchContent[] = data.results.slice(0, 5).map((item) => ({
186
+ content: item.content,
187
+ title: item.title,
188
+ url: item.url,
189
+ }));
190
+
191
+ await get().internal_updateMessageContent(id, JSON.stringify(searchContent));
192
+
193
+ // 如果没搜索到结果,那么不触发 ai 总结
194
+ if (searchContent.length === 0) return;
195
+
196
+ // 如果 aiSummary 为 true,则会自动触发总结
197
+ return aiSummary;
198
+ },
85
199
  text2image: async (id, data) => {
86
200
  // const isAutoGen = settingsSelectors.isDalleAutoGenerating(useGlobalStore.getState());
87
201
  // if (!isAutoGen) return;
88
202
 
89
203
  await get().generateImageFromPrompts(data, id);
90
204
  },
205
+
91
206
  toggleDallEImageLoading: (key, value) => {
92
207
  set(
93
208
  { dalleImageLoading: { ...get().dalleImageLoading, [key]: value } },
@@ -95,6 +210,11 @@ export const chatToolSlice: StateCreator<
95
210
  n('toggleDallEImageLoading'),
96
211
  );
97
212
  },
213
+
214
+ toggleSearchLoading: (id, loading) => {
215
+ set({ searchLoading: { ...get().searchLoading, [id]: loading } }, false, 'toggleSearchLoading');
216
+ },
217
+
98
218
  updateImageItem: async (id, updater) => {
99
219
  const message = chatSelectors.getMessageById(id)(get());
100
220
  if (!message) return;
@@ -104,6 +224,7 @@ export const chatToolSlice: StateCreator<
104
224
  const nextContent = produce(data, updater);
105
225
  await get().internal_updateMessageContent(id, JSON.stringify(nextContent));
106
226
  },
227
+
107
228
  useFetchDalleImageItem: (id) =>
108
229
  useClientDataSWR([SWR_FETCH_KEY, id], async () => {
109
230
  const item = await fileService.getFile(id);
@@ -3,9 +3,11 @@ import { FileItem } from '@/types/files';
3
3
  export interface ChatToolState {
4
4
  dalleImageLoading: Record<string, boolean>;
5
5
  dalleImageMap: Record<string, FileItem>;
6
+ searchLoading: Record<string, boolean>;
6
7
  }
7
8
 
8
9
  export const initialToolState: ChatToolState = {
9
10
  dalleImageLoading: {},
10
11
  dalleImageMap: {},
12
+ searchLoading: {},
11
13
  };
@@ -5,7 +5,10 @@ const isDallEImageGenerating = (id: string) => (s: ChatStoreState) => s.dalleIma
5
5
  const isGeneratingDallEImage = (s: ChatStoreState) =>
6
6
  Object.values(s.dalleImageLoading).some(Boolean);
7
7
 
8
+ const isSearXNGSearching = (id: string) => (s: ChatStoreState) => s.searchLoading[id];
9
+
8
10
  export const chatToolSelectors = {
9
11
  isDallEImageGenerating,
10
12
  isGeneratingDallEImage,
13
+ isSearXNGSearching,
11
14
  };
@@ -15,6 +15,7 @@ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
15
15
  import {
16
16
  ChatMessage,
17
17
  ChatMessageError,
18
+ ChatMessagePluginError,
18
19
  CreateMessageParams,
19
20
  MessageToolCall,
20
21
  ModelReasoning,
@@ -84,6 +85,10 @@ export interface ChatMessageAction {
84
85
  * update the message error with optimistic update
85
86
  */
86
87
  internal_updateMessageError: (id: string, error: ChatMessageError | null) => Promise<void>;
88
+ internal_updateMessagePluginError: (
89
+ id: string,
90
+ error: ChatMessagePluginError | null,
91
+ ) => Promise<void>;
87
92
  /**
88
93
  * create a message with optimistic update
89
94
  */
@@ -276,6 +281,12 @@ export const chatMessage: StateCreator<
276
281
  await messageService.updateMessage(id, { error });
277
282
  await get().refreshMessages();
278
283
  },
284
+
285
+ internal_updateMessagePluginError: async (id, error) => {
286
+ await messageService.updateMessagePluginError(id, error);
287
+ await get().refreshMessages();
288
+ },
289
+
279
290
  internal_updateMessageContent: async (id, content, extra) => {
280
291
  const { internal_dispatchMessage, refreshMessages, internal_transformToolCalls } = get();
281
292
 
@@ -855,7 +855,7 @@ describe('ChatPluginAction', () => {
855
855
  arguments: '{}',
856
856
  },
857
857
  tool_call_id: 'tool-id',
858
- error: { message: 'Previous error', type: 'ProviderBizError' },
858
+ pluginError: { message: 'Previous error', type: 'ProviderBizError' },
859
859
  } as ChatMessage;
860
860
 
861
861
  const internal_updateMessageErrorMock = vi.fn();
@@ -865,7 +865,7 @@ describe('ChatPluginAction', () => {
865
865
  activeId: 'session-id',
866
866
  messagesMap: { [messageMapKey('session-id')]: [message] },
867
867
  internal_invokeDifferentTypePlugin: vi.fn(),
868
- internal_updateMessageError: internal_updateMessageErrorMock,
868
+ internal_updateMessagePluginError: internal_updateMessageErrorMock,
869
869
  });
870
870
  });
871
871
 
@@ -200,8 +200,8 @@ export const chatPlugin: StateCreator<
200
200
  if (!message || message.role !== 'tool' || !message.plugin) return;
201
201
 
202
202
  // if there is error content, then clear the error
203
- if (!!message.error) {
204
- get().internal_updateMessageError(id, null);
203
+ if (!!message.pluginError) {
204
+ get().internal_updateMessagePluginError(id, null);
205
205
  }
206
206
 
207
207
  const payload: ChatToolPayload = { ...message.plugin, id: message.tool_call_id! };
@@ -1,11 +1,11 @@
1
1
  import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
2
- import { uniqBy } from 'lodash-es';
3
2
 
4
3
  import { pluginPrompts } from '@/prompts/plugin';
5
4
  import { MetaData } from '@/types/meta';
6
5
  import { ChatCompletionTool } from '@/types/openai/chat';
7
6
  import { LobeToolMeta } from '@/types/tool/tool';
8
7
  import { genToolCallingName } from '@/utils/toolCall';
8
+ import { convertPluginManifestToToolsCalling } from '@/utils/toolManifest';
9
9
 
10
10
  import { pluginHelpers } from '../helpers';
11
11
  import { ToolStoreState } from '../initialState';
@@ -15,20 +15,13 @@ import { pluginSelectors } from '../slices/plugin/selectors';
15
15
  const enabledSchema =
16
16
  (tools: string[] = []) =>
17
17
  (s: ToolStoreState): ChatCompletionTool[] => {
18
- const list = pluginSelectors
18
+ const manifests = pluginSelectors
19
19
  .installedPluginManifestList(s)
20
20
  .concat(s.builtinTools.map((b) => b.manifest as LobeChatPluginManifest))
21
21
  // 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件
22
- .filter((m) => tools.includes(m?.identifier))
23
- .flatMap((manifest) =>
24
- manifest.api.map((m) => ({
25
- description: m.description,
26
- name: genToolCallingName(manifest.identifier, m.name, manifest.type),
27
- parameters: m.parameters,
28
- })),
29
- );
30
-
31
- return uniqBy(list, 'name').map((i) => ({ function: i, type: 'function' }));
22
+ .filter((m) => tools.includes(m?.identifier));
23
+
24
+ return convertPluginManifestToToolsCalling(manifests);
32
25
  };
33
26
 
34
27
  const enabledSystemRoles =
@@ -7,7 +7,7 @@ const metaList =
7
7
  (showDalle?: boolean) =>
8
8
  (s: ToolStoreState): LobeToolMeta[] =>
9
9
  s.builtinTools
10
- .filter((item) => (!showDalle ? item.identifier !== DalleManifest.identifier : true))
10
+ .filter((item) => (!showDalle ? item.identifier !== DalleManifest.identifier : !item.hidden))
11
11
  .map((t) => ({
12
12
  author: 'LobeHub',
13
13
  identifier: t.identifier,
@@ -50,6 +50,8 @@ export interface ModelListAction {
50
50
  config: Partial<UserKeyVaults[T]>,
51
51
  ) => Promise<void>;
52
52
 
53
+ updateKeyVaultSettings: (key: string, config: any) => Promise<void>;
54
+
53
55
  useFetchProviderModelList: (
54
56
  provider: GlobalLLMProviderKey,
55
57
  enabledAutoFetch: boolean,
@@ -189,6 +191,10 @@ export const createModelListSlice: StateCreator<
189
191
  await get().setSettings({ keyVaults: { [provider]: config } });
190
192
  },
191
193
 
194
+ updateKeyVaultSettings: async (provider, config) => {
195
+ await get().setSettings({ keyVaults: { [provider]: config } });
196
+ },
197
+
192
198
  useFetchProviderModelList: (provider, enabledAutoFetch) =>
193
199
  useSWR<ChatModelCard[] | undefined>(
194
200
  [provider, enabledAutoFetch],
@@ -41,6 +41,7 @@ export const keyVaultsConfigSelectors = {
41
41
  getVaultByProvider,
42
42
  isProviderApiKeyNotEmpty,
43
43
  isProviderEndpointNotEmpty,
44
+ keyVaultsSettings,
44
45
  ollamaConfig,
45
46
  openAIConfig,
46
47
  password,