@lobehub/chat 1.80.5 → 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 (52) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -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/tools.ts +2 -0
  16. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/Debug.tsx +9 -3
  17. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +21 -0
  18. package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments.tsx +1 -1
  19. package/src/locales/default/plugin.ts +1 -0
  20. package/src/server/routers/tools/{__test__/search.test.ts → search.test.ts} +27 -5
  21. package/src/server/routers/tools/search.ts +3 -44
  22. package/src/server/services/search/impls/index.ts +30 -0
  23. package/src/server/services/search/impls/search1api/index.ts +154 -0
  24. package/src/server/services/search/impls/search1api/type.ts +81 -0
  25. package/src/server/{modules/SearXNG.ts → services/search/impls/searxng/client.ts} +32 -2
  26. package/src/server/{routers/tools/__tests__ → services/search/impls/searxng}/fixtures/searXNG.ts +2 -2
  27. package/src/server/services/search/impls/searxng/index.test.ts +26 -0
  28. package/src/server/services/search/impls/searxng/index.ts +62 -0
  29. package/src/server/services/search/impls/type.ts +11 -0
  30. package/src/server/services/search/index.ts +59 -0
  31. package/src/store/chat/slices/builtinTool/actions/index.ts +1 -1
  32. package/src/store/chat/slices/builtinTool/actions/{searXNG.test.ts → search.test.ts} +30 -55
  33. package/src/store/chat/slices/builtinTool/actions/{searXNG.ts → search.ts} +25 -32
  34. package/src/tools/web-browsing/Portal/Search/Footer.tsx +1 -1
  35. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/TitleExtra.tsx +2 -2
  36. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/Video.tsx +9 -7
  37. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/index.tsx +2 -2
  38. package/src/tools/web-browsing/Portal/Search/ResultList/index.tsx +3 -3
  39. package/src/tools/web-browsing/Portal/Search/index.tsx +4 -4
  40. package/src/tools/web-browsing/Portal/index.tsx +3 -1
  41. package/src/tools/web-browsing/Render/Search/SearchQuery/SearchView.tsx +4 -2
  42. package/src/tools/web-browsing/Render/Search/SearchQuery/index.tsx +6 -13
  43. package/src/tools/web-browsing/Render/Search/SearchResult/SearchResultItem.tsx +2 -2
  44. package/src/tools/web-browsing/Render/Search/SearchResult/index.tsx +5 -5
  45. package/src/tools/web-browsing/Render/Search/index.tsx +2 -2
  46. package/src/tools/web-browsing/Render/index.tsx +4 -3
  47. package/src/tools/web-browsing/components/SearchBar.tsx +4 -6
  48. package/src/tools/web-browsing/index.ts +54 -60
  49. package/src/tools/web-browsing/systemRole.ts +22 -13
  50. package/src/types/tool/search/index.ts +44 -0
  51. package/src/server/routers/tools/__tests__/search.test.ts +0 -48
  52. package/src/types/tool/search.ts +0 -48
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The query you want to ask
3
+ */
4
+ export type Query = string;
5
+
6
+ export const SEARCH1API_SUPPORT_SEARCH_SERVICE = [
7
+ 'google',
8
+ 'bing',
9
+ 'duckduckgo',
10
+ 'yahoo',
11
+ 'youtube',
12
+ 'x',
13
+ 'reddit',
14
+ 'github',
15
+ 'arxiv',
16
+ 'wechat',
17
+ 'bilibili',
18
+ 'imdb',
19
+ 'wikipedia',
20
+ ] as const;
21
+
22
+ /**
23
+ * The search service you want to choose
24
+ */
25
+ export type SearchService = (typeof SEARCH1API_SUPPORT_SEARCH_SERVICE)[number];
26
+
27
+ /**
28
+ * The results you want to have
29
+ */
30
+ export type MaxResults = number;
31
+ /**
32
+ * The results you want to crawl
33
+ */
34
+ export type CrawlResults = number;
35
+ /**
36
+ * Search including image urls
37
+ */
38
+ export type Image = boolean;
39
+ /**
40
+ * List of websites to include in search results
41
+ */
42
+ export type IncludeSites = string[];
43
+ /**
44
+ * List of websites to exclude from search results
45
+ */
46
+ export type ExcludeSites = string[];
47
+ /**
48
+ * The language preference for search results (e.g., 'en', 'zh-CN', 'fr'). Uses standard language codes.
49
+ */
50
+ export type Language = string;
51
+ /**
52
+ * Limit search results to a specific time range
53
+ */
54
+ export type TimeRange = 'day' | 'month' | 'year';
55
+
56
+ export interface Search1ApiSearchParameters {
57
+ crawl_results?: CrawlResults;
58
+ exclude_sites?: ExcludeSites;
59
+ image?: Image;
60
+ include_sites?: IncludeSites;
61
+ language?: Language;
62
+ max_results?: MaxResults;
63
+ query: Query;
64
+ search_service?: SearchService;
65
+ time_range?: TimeRange;
66
+ }
67
+
68
+ // Define the Search1API specific response structure based on user input
69
+ // Ideally, this would live in a dedicated types file (e.g., src/types/tool/search/search1api.ts)
70
+ interface Search1ApiResult {
71
+ content?: string;
72
+ link: string;
73
+ snippet?: string;
74
+ title?: string;
75
+ }
76
+
77
+ export interface Search1ApiResponse {
78
+ // Keeping this generic for now
79
+ results?: Search1ApiResult[];
80
+ searchParameters?: Search1ApiSearchParameters;
81
+ }
@@ -1,7 +1,34 @@
1
1
  import qs from 'query-string';
2
2
  import urlJoin from 'url-join';
3
3
 
4
- import { SearchResponse } from '@/types/tool/search';
4
+ export interface SearXNGSearchResult {
5
+ category: string;
6
+ content?: string;
7
+ engine: string;
8
+ engines: string[];
9
+ iframe_src?: string;
10
+ img_src?: string;
11
+ parsed_url: string[];
12
+ positions: number[];
13
+ publishedDate?: string | null;
14
+ score: number;
15
+ template: string;
16
+ thumbnail?: string | null;
17
+ thumbnail_src?: string | null;
18
+ title: string;
19
+ url: string;
20
+ }
21
+
22
+ export interface SearXNGSearchResponse {
23
+ answers: any[];
24
+ corrections: any[];
25
+ infoboxes: any[];
26
+ number_of_results: number;
27
+ query: string;
28
+ results: SearXNGSearchResult[];
29
+ suggestions: string[];
30
+ unresponsive_engines: any[];
31
+ }
5
32
 
6
33
  export class SearXNGClient {
7
34
  private baseUrl: string;
@@ -10,7 +37,10 @@ export class SearXNGClient {
10
37
  this.baseUrl = baseUrl;
11
38
  }
12
39
 
13
- async search(query: string, optionalParams: Record<string, any> = {}): Promise<SearchResponse> {
40
+ async search(
41
+ query: string,
42
+ optionalParams: Record<string, any> = {},
43
+ ): Promise<SearXNGSearchResponse> {
14
44
  try {
15
45
  const { time_range, ...otherParams } = optionalParams;
16
46
 
@@ -1,6 +1,6 @@
1
- import { SearchResponse } from '@/types/tool/search';
1
+ import { SearXNGSearchResponse } from '../client';
2
2
 
3
- export const hetongxue: SearchResponse = {
3
+ export const hetongxue: SearXNGSearchResponse = {
4
4
  answers: [
5
5
  '老师好我叫何同学. 目标是做有意思的视频 合作请联系:xhaxx1123@163.com. 【何同学】我拍了一张600万人的合影...共计2条视频,包括:正片、教程等,UP主更多精彩视频,请关注UP账号。.',
6
6
  ],
@@ -0,0 +1,26 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { SearXNGClient } from './client';
5
+ import { hetongxue } from './fixtures/searXNG';
6
+ import { SearXNGImpl } from './index';
7
+
8
+ vi.mock('@/config/tools', () => ({
9
+ toolsEnv: {
10
+ SEARXNG_URL: 'https://demo.com',
11
+ },
12
+ }));
13
+
14
+ describe('SearXNGImpl', () => {
15
+ describe('query', () => {
16
+ it('搜索结果超过10个', async () => {
17
+ vi.spyOn(SearXNGClient.prototype, 'search').mockResolvedValueOnce(hetongxue);
18
+
19
+ const searchImpl = new SearXNGImpl();
20
+ const results = await searchImpl.query('何同学');
21
+
22
+ // Assert
23
+ expect(results.results.length).toEqual(43);
24
+ });
25
+ });
26
+ });
@@ -0,0 +1,62 @@
1
+ import { TRPCError } from '@trpc/server';
2
+
3
+ import { toolsEnv } from '@/config/tools';
4
+ import { SearXNGClient } from '@/server/services/search/impls/searxng/client';
5
+ import { SEARCH_SEARXNG_NOT_CONFIG, UniformSearchResponse } from '@/types/tool/search';
6
+
7
+ import { SearchServiceImpl } from '../type';
8
+
9
+ /**
10
+ * SearXNG implementation of the search service
11
+ */
12
+ export class SearXNGImpl implements SearchServiceImpl {
13
+ async query(
14
+ query: string,
15
+ params?: {
16
+ searchCategories?: string[];
17
+ searchEngines?: string[];
18
+ searchTimeRange?: string;
19
+ },
20
+ ): Promise<UniformSearchResponse> {
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
+ let costTime = 0;
29
+ const startAt = Date.now();
30
+ const data = await client.search(query, {
31
+ categories: params?.searchCategories,
32
+ engines: params?.searchEngines,
33
+ time_range: params?.searchTimeRange,
34
+ });
35
+ costTime = Date.now() - startAt;
36
+
37
+ return {
38
+ costTime,
39
+ query,
40
+ resultNumbers: data.number_of_results,
41
+ results: data.results.map((item) => ({
42
+ category: item.category,
43
+ content: item.content!,
44
+ engines: item.engines,
45
+ parsedUrl: item.url ? new URL(item.url).hostname : '',
46
+ publishedDate: item.publishedDate || undefined,
47
+ score: item.score,
48
+ thumbnail: item.thumbnail || undefined,
49
+ title: item.title,
50
+ url: item.url,
51
+ })),
52
+ };
53
+ } catch (e) {
54
+ console.error(e);
55
+
56
+ throw new TRPCError({
57
+ code: 'SERVICE_UNAVAILABLE',
58
+ message: (e as Error).message,
59
+ });
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,11 @@
1
+ import { SearchParams, UniformSearchResponse } from '@/types/tool/search';
2
+
3
+ /**
4
+ * Search service implementation interface
5
+ */
6
+ export interface SearchServiceImpl {
7
+ /**
8
+ * Query for search results
9
+ */
10
+ query(query: string, params?: SearchParams): Promise<UniformSearchResponse>;
11
+ }
@@ -0,0 +1,59 @@
1
+ import { CrawlImplType, Crawler } from '@lobechat/web-crawler';
2
+ import pMap from 'p-map';
3
+
4
+ import { toolsEnv } from '@/config/tools';
5
+ import { SearchParams } from '@/types/tool/search';
6
+
7
+ import { SearchImplType, SearchServiceImpl, createSearchServiceImpl } from './impls';
8
+
9
+ const parseImplEnv = (envString: string = '') => {
10
+ // 处理全角逗号和多余空格
11
+ const envValue = envString.replaceAll(',', ',').trim();
12
+ return envValue.split(',').filter(Boolean);
13
+ };
14
+
15
+ /**
16
+ * Search service class
17
+ * Uses different implementations for different search operations
18
+ */
19
+ export class SearchService {
20
+ private searchImpl: SearchServiceImpl;
21
+
22
+ private get crawlerImpls() {
23
+ return parseImplEnv(toolsEnv.CRAWLER_IMPLS);
24
+ }
25
+
26
+ constructor() {
27
+ const impls = this.searchImpls;
28
+ // TODO: need use turn mode
29
+ this.searchImpl = createSearchServiceImpl(impls.length > 0 ? impls[0] : undefined);
30
+ }
31
+
32
+ async crawlPages(input: { impls?: CrawlImplType[]; urls: string[] }) {
33
+ const crawler = new Crawler({ impls: this.crawlerImpls });
34
+
35
+ const results = await pMap(
36
+ input.urls,
37
+ async (url) => {
38
+ return await crawler.crawl({ impls: input.impls, url });
39
+ },
40
+ { concurrency: 3 },
41
+ );
42
+
43
+ return { results };
44
+ }
45
+
46
+ private get searchImpls() {
47
+ return parseImplEnv(toolsEnv.SEARCH_PROVIDERS) as SearchImplType[];
48
+ }
49
+
50
+ /**
51
+ * Query for search results
52
+ */
53
+ async query(query: string, params?: SearchParams) {
54
+ return this.searchImpl.query(query, params);
55
+ }
56
+ }
57
+
58
+ // Add a default exported instance for convenience
59
+ export const searchService = new SearchService();
@@ -3,7 +3,7 @@ import { StateCreator } from 'zustand/vanilla';
3
3
  import { ChatStore } from '@/store/chat/store';
4
4
 
5
5
  import { ChatDallEAction, dalleSlice } from './dalle';
6
- import { SearchAction, searchSlice } from './searXNG';
6
+ import { SearchAction, searchSlice } from './search';
7
7
 
8
8
  export interface ChatBuiltinToolAction extends ChatDallEAction, SearchAction {}
9
9
 
@@ -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
  });