@lobehub/chat 1.80.4 → 1.81.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 (59) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/electron-client-ipc/src/events/index.ts +6 -2
  5. package/packages/electron-client-ipc/src/events/remoteServer.ts +28 -0
  6. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  7. package/packages/electron-client-ipc/src/types/remoteServer.ts +8 -0
  8. package/packages/electron-server-ipc/package.json +7 -1
  9. package/packages/electron-server-ipc/src/ipcClient.ts +54 -20
  10. package/packages/electron-server-ipc/src/ipcServer.ts +42 -9
  11. package/packages/web-crawler/src/crawImpl/__tests__/search1api.test.ts +33 -39
  12. package/packages/web-crawler/src/crawImpl/search1api.ts +1 -7
  13. package/packages/web-crawler/src/index.ts +1 -0
  14. package/packages/web-crawler/src/urlRules.ts +3 -1
  15. package/src/config/aiModels/ai21.ts +10 -6
  16. package/src/config/aiModels/ai360.ts +36 -2
  17. package/src/config/aiModels/stepfun.ts +1 -0
  18. package/src/config/aiModels/taichu.ts +61 -0
  19. package/src/config/aiModels/volcengine.ts +0 -1
  20. package/src/config/modelProviders/ai21.ts +1 -1
  21. package/src/config/tools.ts +2 -0
  22. package/src/database/repositories/aiInfra/index.test.ts +3 -3
  23. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/Debug.tsx +9 -3
  24. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +21 -0
  25. package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments.tsx +1 -1
  26. package/src/locales/default/plugin.ts +1 -0
  27. package/src/server/routers/tools/{__test__/search.test.ts → search.test.ts} +27 -5
  28. package/src/server/routers/tools/search.ts +3 -44
  29. package/src/server/services/search/impls/index.ts +30 -0
  30. package/src/server/services/search/impls/search1api/index.ts +154 -0
  31. package/src/server/services/search/impls/search1api/type.ts +81 -0
  32. package/src/server/{modules/SearXNG.ts → services/search/impls/searxng/client.ts} +32 -2
  33. package/src/server/{routers/tools/__tests__ → services/search/impls/searxng}/fixtures/searXNG.ts +2 -2
  34. package/src/server/services/search/impls/searxng/index.test.ts +26 -0
  35. package/src/server/services/search/impls/searxng/index.ts +62 -0
  36. package/src/server/services/search/impls/type.ts +11 -0
  37. package/src/server/services/search/index.ts +59 -0
  38. package/src/store/chat/slices/builtinTool/actions/index.ts +1 -1
  39. package/src/store/chat/slices/builtinTool/actions/{searXNG.test.ts → search.test.ts} +30 -55
  40. package/src/store/chat/slices/builtinTool/actions/{searXNG.ts → search.ts} +25 -32
  41. package/src/tools/web-browsing/Portal/Search/Footer.tsx +1 -1
  42. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/TitleExtra.tsx +2 -2
  43. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/Video.tsx +9 -7
  44. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/index.tsx +2 -2
  45. package/src/tools/web-browsing/Portal/Search/ResultList/index.tsx +3 -3
  46. package/src/tools/web-browsing/Portal/Search/index.tsx +4 -4
  47. package/src/tools/web-browsing/Portal/index.tsx +3 -1
  48. package/src/tools/web-browsing/Render/Search/SearchQuery/SearchView.tsx +4 -2
  49. package/src/tools/web-browsing/Render/Search/SearchQuery/index.tsx +6 -13
  50. package/src/tools/web-browsing/Render/Search/SearchResult/SearchResultItem.tsx +2 -2
  51. package/src/tools/web-browsing/Render/Search/SearchResult/index.tsx +5 -5
  52. package/src/tools/web-browsing/Render/Search/index.tsx +2 -2
  53. package/src/tools/web-browsing/Render/index.tsx +4 -3
  54. package/src/tools/web-browsing/components/SearchBar.tsx +4 -6
  55. package/src/tools/web-browsing/index.ts +54 -60
  56. package/src/tools/web-browsing/systemRole.ts +22 -13
  57. package/src/types/tool/search/index.ts +44 -0
  58. package/src/server/routers/tools/__tests__/search.test.ts +0 -48
  59. package/src/types/tool/search.ts +0 -48
@@ -6,7 +6,7 @@ import { useChatStore } from '@/store/chat';
6
6
  import { chatSelectors } from '@/store/chat/selectors';
7
7
  import { CRAWL_CONTENT_LIMITED_COUNT } from '@/tools/web-browsing/const';
8
8
  import { ChatMessage } from '@/types/message';
9
- import { SearchContent, SearchQuery, SearchResponse } from '@/types/tool/search';
9
+ import { SearchContent, SearchQuery, UniformSearchResponse } from '@/types/tool/search';
10
10
 
11
11
  // Mock services
12
12
  vi.mock('@/services/search', () => ({
@@ -22,7 +22,7 @@ vi.mock('@/store/chat/selectors', () => ({
22
22
  },
23
23
  }));
24
24
 
25
- describe('searXNG actions', () => {
25
+ describe('search actions', () => {
26
26
  beforeEach(() => {
27
27
  vi.clearAllMocks();
28
28
  useChatStore.setState({
@@ -39,47 +39,38 @@ describe('searXNG actions', () => {
39
39
  });
40
40
  });
41
41
 
42
- describe('searchWithSearXNG', () => {
42
+ describe('search', () => {
43
43
  it('should handle successful search', async () => {
44
- const mockResponse: SearchResponse = {
44
+ const mockResponse: UniformSearchResponse = {
45
45
  results: [
46
46
  {
47
47
  title: 'Test Result',
48
48
  content: 'Test Content',
49
49
  url: 'https://test.com',
50
50
  category: 'general',
51
- engine: 'google',
52
51
  engines: ['google'],
53
- parsed_url: ['test.com'],
54
- positions: [1],
52
+ parsedUrl: 'test.com',
55
53
  score: 1,
56
- template: 'default',
57
54
  },
58
55
  ],
59
- answers: [],
60
- corrections: [],
61
- infoboxes: [],
62
- number_of_results: 1,
56
+ costTime: 1,
57
+ resultNumbers: 1,
63
58
  query: 'test',
64
- suggestions: [],
65
- unresponsive_engines: [],
66
59
  };
67
60
 
68
61
  (searchService.search as Mock).mockResolvedValue(mockResponse);
69
62
 
70
63
  const { result } = renderHook(() => useChatStore());
71
- const { searchWithSearXNG } = result.current;
64
+ const { search } = result.current;
72
65
 
73
66
  const messageId = 'test-message-id';
74
67
  const query: SearchQuery = {
75
- optionalParams: {
76
- searchEngines: ['google'],
77
- },
68
+ searchEngines: ['google'],
78
69
  query: 'test query',
79
70
  };
80
71
 
81
72
  await act(async () => {
82
- await searchWithSearXNG(messageId, query);
73
+ await search(messageId, query);
83
74
  });
84
75
 
85
76
  const expectedContent: SearchContent[] = [
@@ -101,39 +92,28 @@ describe('searXNG actions', () => {
101
92
  });
102
93
 
103
94
  it('should handle empty search results and retry with default engine', async () => {
104
- const emptyResponse: SearchResponse = {
95
+ const emptyResponse: UniformSearchResponse = {
105
96
  results: [],
106
- answers: [],
107
- corrections: [],
108
- infoboxes: [],
109
- number_of_results: 0,
97
+ costTime: 1,
98
+ resultNumbers: 0,
110
99
  query: 'test',
111
- suggestions: [],
112
- unresponsive_engines: [],
113
100
  };
114
101
 
115
- const retryResponse: SearchResponse = {
102
+ const retryResponse: UniformSearchResponse = {
116
103
  results: [
117
104
  {
118
105
  title: 'Retry Result',
119
106
  content: 'Retry Content',
120
107
  url: 'https://retry.com',
121
108
  category: 'general',
122
- engine: 'google',
123
109
  engines: ['google'],
124
- parsed_url: ['retry.com'],
125
- positions: [1],
110
+ parsedUrl: 'retry.com',
126
111
  score: 1,
127
- template: 'default',
128
112
  },
129
113
  ],
130
- answers: [],
131
- corrections: [],
132
- infoboxes: [],
133
- number_of_results: 1,
114
+ costTime: 1,
115
+ resultNumbers: 1,
134
116
  query: 'test',
135
- suggestions: [],
136
- unresponsive_engines: [],
137
117
  };
138
118
 
139
119
  (searchService.search as Mock)
@@ -142,19 +122,17 @@ describe('searXNG actions', () => {
142
122
  .mockResolvedValueOnce(retryResponse);
143
123
 
144
124
  const { result } = renderHook(() => useChatStore());
145
- const { searchWithSearXNG } = result.current;
125
+ const { search } = result.current;
146
126
 
147
127
  const messageId = 'test-message-id';
148
128
  const query: SearchQuery = {
149
- optionalParams: {
150
- searchEngines: ['custom-engine'],
151
- searchTimeRange: 'year',
152
- },
129
+ searchEngines: ['custom-engine'],
130
+ searchTimeRange: 'year',
153
131
  query: 'test query',
154
132
  };
155
133
 
156
134
  await act(async () => {
157
- await searchWithSearXNG(messageId, query);
135
+ await search(messageId, query);
158
136
  });
159
137
 
160
138
  expect(searchService.search).toHaveBeenCalledTimes(3);
@@ -166,9 +144,6 @@ describe('searXNG actions', () => {
166
144
  searchTimeRange: 'year',
167
145
  });
168
146
  expect(result.current.updatePluginArguments).toHaveBeenCalledWith(messageId, {
169
- optionalParams: {
170
- searchTimeRange: 'year',
171
- },
172
147
  query: 'test query',
173
148
  });
174
149
  expect(searchService.search).toHaveBeenNthCalledWith(3, 'test query');
@@ -183,7 +158,7 @@ describe('searXNG actions', () => {
183
158
  (searchService.search as Mock).mockRejectedValue(error);
184
159
 
185
160
  const { result } = renderHook(() => useChatStore());
186
- const { searchWithSearXNG } = result.current;
161
+ const { search } = result.current;
187
162
 
188
163
  const messageId = 'test-message-id';
189
164
  const query: SearchQuery = {
@@ -191,7 +166,7 @@ describe('searXNG actions', () => {
191
166
  };
192
167
 
193
168
  await act(async () => {
194
- await searchWithSearXNG(messageId, query);
169
+ await search(messageId, query);
195
170
  });
196
171
 
197
172
  expect(result.current.internal_updateMessagePluginError).toHaveBeenCalledWith(messageId, {
@@ -271,8 +246,8 @@ describe('searXNG actions', () => {
271
246
  describe('reSearchWithSearXNG', () => {
272
247
  it('should update arguments and perform search', async () => {
273
248
  const { result } = renderHook(() => useChatStore());
274
- const spy = vi.spyOn(result.current, 'searchWithSearXNG');
275
- const { reSearchWithSearXNG } = result.current;
249
+ const spy = vi.spyOn(result.current, 'search');
250
+ const { triggerSearchAgain } = result.current;
276
251
 
277
252
  const messageId = 'test-message-id';
278
253
  const query: SearchQuery = {
@@ -280,7 +255,7 @@ describe('searXNG actions', () => {
280
255
  };
281
256
 
282
257
  await act(async () => {
283
- await reSearchWithSearXNG(messageId, query, { aiSummary: true });
258
+ await triggerSearchAgain(messageId, query, { aiSummary: true });
284
259
  });
285
260
 
286
261
  expect(result.current.updatePluginArguments).toHaveBeenCalledWith(messageId, query);
@@ -314,10 +289,10 @@ describe('searXNG actions', () => {
314
289
  );
315
290
 
316
291
  const { result } = renderHook(() => useChatStore());
317
- const { saveSearXNGSearchResult } = result.current;
292
+ const { saveSearchResult } = result.current;
318
293
 
319
294
  await act(async () => {
320
- await saveSearXNGSearchResult(messageId);
295
+ await saveSearchResult(messageId);
321
296
  });
322
297
 
323
298
  expect(result.current.internal_createMessage).toHaveBeenCalledWith(
@@ -343,10 +318,10 @@ describe('searXNG actions', () => {
343
318
  vi.spyOn(chatSelectors, 'getMessageById').mockImplementation(() => () => undefined);
344
319
 
345
320
  const { result } = renderHook(() => useChatStore());
346
- const { saveSearXNGSearchResult } = result.current;
321
+ const { saveSearchResult } = result.current;
347
322
 
348
323
  await act(async () => {
349
- await saveSearXNGSearchResult('non-existent-id');
324
+ await saveSearchResult('non-existent-id');
350
325
  });
351
326
 
352
327
  expect(result.current.internal_createMessage).not.toHaveBeenCalled();
@@ -9,7 +9,7 @@ import {
9
9
  SEARCH_SEARXNG_NOT_CONFIG,
10
10
  SearchContent,
11
11
  SearchQuery,
12
- SearchResponse,
12
+ UniformSearchResponse,
13
13
  } from '@/types/tool/search';
14
14
  import { nanoid } from '@/utils/uuid';
15
15
 
@@ -24,23 +24,19 @@ export interface SearchAction {
24
24
  params: { url: string },
25
25
  aiSummary?: boolean,
26
26
  ) => Promise<boolean | undefined>;
27
+ saveSearchResult: (id: string) => Promise<void>;
28
+ search: (id: string, data: SearchQuery, aiSummary?: boolean) => Promise<void | boolean>;
29
+ togglePageContent: (url: string) => void;
30
+ toggleSearchLoading: (id: string, loading: boolean) => void;
27
31
  /**
28
32
  * 重新发起搜索
29
33
  * @description 会更新插件的 arguments 参数,然后再次搜索
30
34
  */
31
- reSearchWithSearXNG: (
35
+ triggerSearchAgain: (
32
36
  id: string,
33
37
  data: SearchQuery,
34
38
  options?: { aiSummary: boolean },
35
39
  ) => Promise<void>;
36
- saveSearXNGSearchResult: (id: string) => Promise<void>;
37
- searchWithSearXNG: (
38
- id: string,
39
- data: SearchQuery,
40
- aiSummary?: boolean,
41
- ) => Promise<void | boolean>;
42
- togglePageContent: (url: string) => void;
43
- toggleSearchLoading: (id: string, loading: boolean) => void;
44
40
  }
45
41
 
46
42
  export const searchSlice: StateCreator<
@@ -91,14 +87,7 @@ export const searchSlice: StateCreator<
91
87
  return await crawlMultiPages(id, { urls: [params.url] }, aiSummary);
92
88
  },
93
89
 
94
- reSearchWithSearXNG: async (id, data, options) => {
95
- get().toggleSearchLoading(id, true);
96
- await get().updatePluginArguments(id, data);
97
-
98
- await get().searchWithSearXNG(id, data, options?.aiSummary);
99
- },
100
-
101
- saveSearXNGSearchResult: async (id) => {
90
+ saveSearchResult: async (id) => {
102
91
  const message = chatSelectors.getMessageById(id)(get());
103
92
  if (!message || !message.plugin) return;
104
93
 
@@ -138,34 +127,32 @@ export const searchSlice: StateCreator<
138
127
  // 将新创建的 tool message 激活
139
128
  openToolUI(newMessageId, message.plugin.identifier);
140
129
  },
141
- searchWithSearXNG: async (id, params, aiSummary = true) => {
130
+
131
+ search: async (id, { query, ...params }, aiSummary = true) => {
142
132
  get().toggleSearchLoading(id, true);
143
- let data: SearchResponse | undefined;
133
+ let data: UniformSearchResponse | undefined;
144
134
  try {
145
135
  // 首次查询
146
- data = await searchService.search(params.query, params.optionalParams);
136
+ data = await searchService.search(query, params);
147
137
 
148
138
  // 如果没有搜索到结果,则执行第一次重试(移除搜索引擎限制)
149
139
  if (
150
140
  data?.results.length === 0 &&
151
- params.optionalParams?.searchEngines &&
152
- params.optionalParams?.searchEngines?.length > 0
141
+ params?.searchEngines &&
142
+ params?.searchEngines?.length > 0
153
143
  ) {
154
144
  const paramsExcludeSearchEngines = {
155
145
  ...params,
156
- optionalParams: {
157
- ...params.optionalParams,
158
- searchEngines: undefined,
159
- },
146
+ searchEngines: undefined,
160
147
  };
161
- data = await searchService.search(params.query, paramsExcludeSearchEngines.optionalParams);
148
+ data = await searchService.search(query, paramsExcludeSearchEngines);
162
149
  get().updatePluginArguments(id, paramsExcludeSearchEngines);
163
150
  }
164
151
 
165
152
  // 如果仍然没有搜索到结果,则执行第二次重试(移除所有限制)
166
153
  if (data?.results.length === 0) {
167
- data = await searchService.search(params.query);
168
- get().updatePluginArguments(id, { ...params, optionalParams: undefined });
154
+ data = await searchService.search(query);
155
+ get().updatePluginArguments(id, { query });
169
156
  }
170
157
 
171
158
  await get().updatePluginState(id, data);
@@ -197,7 +184,7 @@ export const searchSlice: StateCreator<
197
184
  url: item.url,
198
185
  ...(item.content && { content: item.content }),
199
186
  ...(item.publishedDate && { publishedDate: item.publishedDate }),
200
- ...(item.img_src && { img_src: item.img_src }),
187
+ ...(item.imgSrc && { imgSrc: item.imgSrc }),
201
188
  ...(item.thumbnail && { thumbnail: item.thumbnail }),
202
189
  }));
203
190
 
@@ -209,7 +196,6 @@ export const searchSlice: StateCreator<
209
196
  // 如果 aiSummary 为 true,则会自动触发总结
210
197
  return aiSummary;
211
198
  },
212
-
213
199
  togglePageContent: (url) => {
214
200
  set({ activePageContentUrl: url });
215
201
  },
@@ -221,4 +207,11 @@ export const searchSlice: StateCreator<
221
207
  `toggleSearchLoading/${loading ? 'start' : 'end'}`,
222
208
  );
223
209
  },
210
+
211
+ triggerSearchAgain: async (id, data, options) => {
212
+ get().toggleSearchLoading(id, true);
213
+ await get().updatePluginArguments(id, data);
214
+
215
+ await get().search(id, data, options?.aiSummary);
216
+ },
224
217
  });
@@ -12,7 +12,7 @@ const Footer = () => {
12
12
  chatPortalSelectors.toolMessageId(s),
13
13
  chatSelectors.isAIGenerating(s),
14
14
  s.triggerAIMessage,
15
- s.saveSearXNGSearchResult,
15
+ s.saveSearchResult,
16
16
  ]);
17
17
 
18
18
  const { t } = useTranslation('tool');
@@ -9,7 +9,7 @@ import { EngineAvatarGroup } from '@/tools/web-browsing/components/EngineAvatar'
9
9
  import CategoryAvatar from './CategoryAvatar';
10
10
 
11
11
  interface TitleExtraProps {
12
- category: string;
12
+ category?: string;
13
13
  engines: string[];
14
14
  highlight?: boolean;
15
15
  score: number;
@@ -35,7 +35,7 @@ const TitleExtra = memo<TitleExtraProps>(({ category, score, highlight, engines
35
35
  </Typography.Text>
36
36
  )}
37
37
  </Tooltip>
38
- <CategoryAvatar category={category} />
38
+ <CategoryAvatar category={category || 'general'} />
39
39
  </Flexbox>
40
40
  );
41
41
  });
@@ -3,7 +3,7 @@ import { createStyles } from 'antd-style';
3
3
  import { memo, useState } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
- import { SearchResult } from '@/types/tool/search';
6
+ import { UniformSearchResult } from '@/types/tool/search';
7
7
 
8
8
  import { ENGINE_ICON_MAP } from '../../../../const';
9
9
  import TitleExtra from './TitleExtra';
@@ -61,19 +61,21 @@ const useStyles = createStyles(({ css, token }) => {
61
61
  };
62
62
  });
63
63
 
64
- interface SearchResultProps extends SearchResult {
64
+ interface SearchResultProps extends UniformSearchResult {
65
65
  highlight?: boolean;
66
66
  }
67
67
  const VideoItem = memo<SearchResultProps>(
68
- ({ content, url, iframe_src, highlight, score, engines, title, category }) => {
68
+ ({ content, url, iframeSrc, highlight, score, engines, title, category, ...res }) => {
69
69
  const { styles, theme } = useStyles();
70
70
 
71
71
  const [expand, setExpand] = useState(false);
72
+
73
+ const videoUrl = iframeSrc || (res as any).iframe_src; // iframe_src 是 SearchXNG 的字段,兼容老的数据结构
72
74
  return (
73
75
  <Flexbox gap={12}>
74
76
  <Flexbox className={styles.container} onClick={() => setExpand(!expand)}>
75
77
  <Flexbox flex={1} gap={8} horizontal padding={12}>
76
- {iframe_src && (
78
+ {videoUrl && (
77
79
  <Flexbox>
78
80
  <iframe
79
81
  // alt={title}
@@ -86,7 +88,7 @@ const VideoItem = memo<SearchResultProps>(
86
88
  onPlay={(e) => {
87
89
  e.preventDefault();
88
90
  }}
89
- src={iframe_src}
91
+ src={videoUrl}
90
92
  style={{
91
93
  pointerEvents: 'none',
92
94
  }}
@@ -127,9 +129,9 @@ const VideoItem = memo<SearchResultProps>(
127
129
  </Flexbox>
128
130
  </Flexbox>
129
131
  </Flexbox>
130
- {expand && iframe_src && (
132
+ {expand && videoUrl && (
131
133
  <Flexbox>
132
- <iframe className={styles.iframe} height={440} src={iframe_src} width={'100%'} />
134
+ <iframe className={styles.iframe} height={440} src={videoUrl} width={'100%'} />
133
135
  </Flexbox>
134
136
  )}
135
137
  </Flexbox>
@@ -4,7 +4,7 @@ import { memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
6
  import WebFavicon from '@/components/WebFavicon';
7
- import { SearchResult } from '@/types/tool/search';
7
+ import { UniformSearchResult } from '@/types/tool/search';
8
8
 
9
9
  import TitleExtra from './TitleExtra';
10
10
  import Video from './Video';
@@ -52,7 +52,7 @@ const useStyles = createStyles(({ css, token }) => {
52
52
  };
53
53
  });
54
54
 
55
- interface SearchResultProps extends SearchResult {
55
+ interface SearchResultProps extends UniformSearchResult {
56
56
  highlight?: boolean;
57
57
  }
58
58
 
@@ -1,17 +1,17 @@
1
1
  import React, { memo, useCallback } from 'react';
2
2
  import { Virtuoso } from 'react-virtuoso';
3
3
 
4
- import { SearchResult } from '@/types/tool/search';
4
+ import { UniformSearchResult } from '@/types/tool/search';
5
5
 
6
6
  import Item from './SearchItem';
7
7
 
8
8
  interface ResultListProps {
9
- dataSources: SearchResult[];
9
+ dataSources: UniformSearchResult[];
10
10
  }
11
11
 
12
12
  const ResultList = memo<ResultListProps>(({ dataSources }) => {
13
13
  const itemContent = useCallback(
14
- (index: number, result: SearchResult) => <Item {...result} highlight={index < 15} />,
14
+ (index: number, result: UniformSearchResult) => <Item {...result} highlight={index < 15} />,
15
15
  [],
16
16
  );
17
17
 
@@ -5,7 +5,7 @@ import { Flexbox } from 'react-layout-kit';
5
5
 
6
6
  import { useChatStore } from '@/store/chat';
7
7
  import { chatToolSelectors } from '@/store/chat/selectors';
8
- import { SearchQuery, SearchResponse } from '@/types/tool/search';
8
+ import { SearchQuery, UniformSearchResponse } from '@/types/tool/search';
9
9
 
10
10
  import SearchBar from '../../components/SearchBar';
11
11
  import Footer from './Footer';
@@ -14,12 +14,12 @@ import ResultList from './ResultList';
14
14
  interface InspectorUIProps {
15
15
  messageId: string;
16
16
  query: SearchQuery;
17
- response: SearchResponse;
17
+ response: UniformSearchResponse;
18
18
  }
19
19
 
20
20
  const Inspector = memo<InspectorUIProps>(({ query: args, messageId, response }) => {
21
- const engines = uniq((response.results || []).map((result) => result.engine));
22
- const defaultEngines = engines.length > 0 ? engines : args.optionalParams?.searchEngines || [];
21
+ const engines = uniq((response.results || []).flatMap((result) => result.engines));
22
+ const defaultEngines = engines.length > 0 ? engines : args?.searchEngines || [];
23
23
  const loading = useChatStore(chatToolSelectors.isSearXNGSearching(messageId));
24
24
 
25
25
  if (loading) {
@@ -11,7 +11,9 @@ import Search from './Search';
11
11
 
12
12
  const Inspector = memo<BuiltinPortalProps>(({ arguments: args, messageId, state, apiName }) => {
13
13
  switch (apiName) {
14
- case WebBrowsingApiName.searchWithSearXNG: {
14
+ // 兼容旧版数据
15
+ case 'searchWithSearXNG':
16
+ case WebBrowsingApiName.search: {
15
17
  return <Search messageId={messageId} query={args as SearchQuery} response={state} />;
16
18
  }
17
19
 
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
9
  import { useIsMobile } from '@/hooks/useIsMobile';
10
+ import { shinyTextStylish } from '@/styles/loading';
10
11
 
11
12
  import { EngineAvatarGroup } from '../../../components/EngineAvatar';
12
13
 
@@ -29,6 +30,7 @@ const useStyles = createStyles(({ css, token }) => ({
29
30
  background: ${token.colorFillTertiary};
30
31
  }
31
32
  `,
33
+ shinyText: shinyTextStylish(token),
32
34
  }));
33
35
 
34
36
  interface SearchBarProps {
@@ -43,7 +45,7 @@ const SearchBar = memo<SearchBarProps>(
43
45
  ({ defaultEngines, defaultQuery, resultsNumber, onEditingChange, searching }) => {
44
46
  const { t } = useTranslation('tool');
45
47
  const isMobile = useIsMobile();
46
- const { styles } = useStyles();
48
+ const { styles, cx } = useStyles();
47
49
  return (
48
50
  <Flexbox
49
51
  align={isMobile ? 'flex-start' : 'center'}
@@ -54,7 +56,7 @@ const SearchBar = memo<SearchBarProps>(
54
56
  >
55
57
  <Flexbox
56
58
  align={'center'}
57
- className={styles.query}
59
+ className={cx(styles.query, searching && styles.shinyText)}
58
60
  gap={8}
59
61
  horizontal
60
62
  onClick={() => {
@@ -1,14 +1,12 @@
1
1
  import { ActionIcon } from '@lobehub/ui';
2
- import { Skeleton } from 'antd';
3
2
  import { uniq } from 'lodash-es';
4
3
  import { XIcon } from 'lucide-react';
5
4
  import { memo } from 'react';
6
5
  import { useTranslation } from 'react-i18next';
7
- import { Flexbox } from 'react-layout-kit';
8
6
 
9
7
  import { useChatStore } from '@/store/chat';
10
8
  import { chatToolSelectors } from '@/store/chat/selectors';
11
- import { SearchQuery, SearchResponse } from '@/types/tool/search';
9
+ import { SearchQuery, UniformSearchResponse } from '@/types/tool/search';
12
10
 
13
11
  import SearchBar from '../../../components/SearchBar';
14
12
  import SearchView from './SearchView';
@@ -17,7 +15,7 @@ interface SearchQueryViewProps {
17
15
  args: SearchQuery;
18
16
  editing: boolean;
19
17
  messageId: string;
20
- pluginState?: SearchResponse;
18
+ pluginState?: UniformSearchResponse;
21
19
  setEditing: (editing: boolean) => void;
22
20
  }
23
21
 
@@ -28,15 +26,10 @@ const SearchQueryView = memo<SearchQueryViewProps>(
28
26
 
29
27
  const { t } = useTranslation('common');
30
28
 
31
- const engines = uniq(searchResults.map((result) => result.engine));
32
- const defaultEngines = engines.length > 0 ? engines : args.optionalParams?.searchEngines || [];
29
+ const engines = uniq(searchResults.flatMap((result) => result.engines));
30
+ const defaultEngines = engines.length > 0 ? engines : args.searchEngines || [];
33
31
 
34
- return !pluginState ? (
35
- <Flexbox align={'center'} distribution={'space-between'} height={32} horizontal>
36
- <Skeleton.Button active style={{ borderRadius: 4, height: 32, width: 180 }} />
37
- <Skeleton.Button active style={{ borderRadius: 4, height: 32, width: 220 }} />
38
- </Flexbox>
39
- ) : editing ? (
32
+ return editing ? (
40
33
  <SearchBar
41
34
  defaultEngines={defaultEngines}
42
35
  defaultQuery={args?.query}
@@ -52,7 +45,7 @@ const SearchQueryView = memo<SearchQueryViewProps>(
52
45
  defaultQuery={args?.query}
53
46
  onEditingChange={setEditing}
54
47
  resultsNumber={searchResults.length}
55
- searching={loading}
48
+ searching={loading || !pluginState}
56
49
  />
57
50
  );
58
51
  },
@@ -5,7 +5,7 @@ import { memo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import WebFavicon from '@/components/WebFavicon';
8
- import { SearchResult } from '@/types/tool/search';
8
+ import { UniformSearchResult } from '@/types/tool/search';
9
9
 
10
10
  const useStyles = createStyles(({ css, token }) => ({
11
11
  container: css`
@@ -43,7 +43,7 @@ const useStyles = createStyles(({ css, token }) => ({
43
43
  `,
44
44
  }));
45
45
 
46
- const SearchResultItem = memo<SearchResult>(({ url, title }) => {
46
+ const SearchResultItem = memo<UniformSearchResult>(({ url, title }) => {
47
47
  const { styles } = useStyles();
48
48
 
49
49
  const urlObj = new URL(url);
@@ -9,7 +9,7 @@ import { Center, Flexbox } from 'react-layout-kit';
9
9
  import { useIsMobile } from '@/hooks/useIsMobile';
10
10
  import { useChatStore } from '@/store/chat';
11
11
  import { chatToolSelectors } from '@/store/chat/selectors';
12
- import { SearchQuery, SearchResponse } from '@/types/tool/search';
12
+ import { SearchQuery, UniformSearchResponse } from '@/types/tool/search';
13
13
 
14
14
  import SearchResultItem from './SearchResultItem';
15
15
  import ShowMore from './ShowMore';
@@ -21,7 +21,7 @@ interface SearchResultProps {
21
21
  args: SearchQuery;
22
22
  editing: boolean;
23
23
  messageId: string;
24
- pluginState?: SearchResponse;
24
+ pluginState?: UniformSearchResponse;
25
25
  setEditing: (editing: boolean) => void;
26
26
  }
27
27
 
@@ -31,14 +31,14 @@ const SearchResult = memo<SearchResultProps>(
31
31
  const searchResults = pluginState?.results || [];
32
32
  const { t } = useTranslation(['tool', 'common']);
33
33
 
34
- const engines = uniq(searchResults.map((result) => result.engine));
35
- const defaultEngines = engines.length > 0 ? engines : args.optionalParams?.searchEngines || [];
34
+ const engines = uniq(searchResults.flatMap((result) => result.engines));
35
+ const defaultEngines = engines.length > 0 ? engines : args?.searchEngines || [];
36
36
  const isMobile = useIsMobile();
37
37
 
38
38
  if (loading || !pluginState)
39
39
  return (
40
40
  <Flexbox gap={12} horizontal>
41
- {['1', '2', '3', '4'].map((id) => (
41
+ {['1', '2', '3', '4', '5'].map((id) => (
42
42
  <Skeleton.Button
43
43
  active
44
44
  key={id}
@@ -3,7 +3,7 @@ import { memo, useState } from 'react';
3
3
  import { Flexbox } from 'react-layout-kit';
4
4
 
5
5
  import { ChatMessagePluginError } from '@/types/message';
6
- import { SearchQuery, SearchResponse } from '@/types/tool/search';
6
+ import { SearchQuery, UniformSearchResponse } from '@/types/tool/search';
7
7
 
8
8
  import ConfigForm from './ConfigForm';
9
9
  import SearchQueryView from './SearchQuery';
@@ -13,7 +13,7 @@ interface SearchProps {
13
13
  messageId: string;
14
14
  pluginError: ChatMessagePluginError;
15
15
  searchQuery: SearchQuery;
16
- searchResponse?: SearchResponse;
16
+ searchResponse?: UniformSearchResponse;
17
17
  }
18
18
 
19
19
  const Search = memo<SearchProps>(({ messageId, searchQuery, searchResponse, pluginError }) => {