@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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/index.ts +6 -2
- package/packages/electron-client-ipc/src/events/remoteServer.ts +28 -0
- package/packages/electron-client-ipc/src/types/index.ts +1 -0
- package/packages/electron-client-ipc/src/types/remoteServer.ts +8 -0
- package/packages/electron-server-ipc/package.json +7 -1
- package/packages/electron-server-ipc/src/ipcClient.ts +54 -20
- package/packages/electron-server-ipc/src/ipcServer.ts +42 -9
- package/packages/web-crawler/src/crawImpl/__tests__/search1api.test.ts +33 -39
- package/packages/web-crawler/src/crawImpl/search1api.ts +1 -7
- package/packages/web-crawler/src/index.ts +1 -0
- package/packages/web-crawler/src/urlRules.ts +3 -1
- package/src/config/tools.ts +2 -0
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/Debug.tsx +9 -3
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +21 -0
- package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments.tsx +1 -1
- package/src/locales/default/plugin.ts +1 -0
- package/src/server/routers/tools/{__test__/search.test.ts → search.test.ts} +27 -5
- package/src/server/routers/tools/search.ts +3 -44
- package/src/server/services/search/impls/index.ts +30 -0
- package/src/server/services/search/impls/search1api/index.ts +154 -0
- package/src/server/services/search/impls/search1api/type.ts +81 -0
- package/src/server/{modules/SearXNG.ts → services/search/impls/searxng/client.ts} +32 -2
- package/src/server/{routers/tools/__tests__ → services/search/impls/searxng}/fixtures/searXNG.ts +2 -2
- package/src/server/services/search/impls/searxng/index.test.ts +26 -0
- package/src/server/services/search/impls/searxng/index.ts +62 -0
- package/src/server/services/search/impls/type.ts +11 -0
- package/src/server/services/search/index.ts +59 -0
- package/src/store/chat/slices/builtinTool/actions/index.ts +1 -1
- package/src/store/chat/slices/builtinTool/actions/{searXNG.test.ts → search.test.ts} +30 -55
- package/src/store/chat/slices/builtinTool/actions/{searXNG.ts → search.ts} +25 -32
- package/src/tools/web-browsing/Portal/Search/Footer.tsx +1 -1
- package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/TitleExtra.tsx +2 -2
- package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/Video.tsx +9 -7
- package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/index.tsx +2 -2
- package/src/tools/web-browsing/Portal/Search/ResultList/index.tsx +3 -3
- package/src/tools/web-browsing/Portal/Search/index.tsx +4 -4
- package/src/tools/web-browsing/Portal/index.tsx +3 -1
- package/src/tools/web-browsing/Render/Search/SearchQuery/SearchView.tsx +4 -2
- package/src/tools/web-browsing/Render/Search/SearchQuery/index.tsx +6 -13
- package/src/tools/web-browsing/Render/Search/SearchResult/SearchResultItem.tsx +2 -2
- package/src/tools/web-browsing/Render/Search/SearchResult/index.tsx +5 -5
- package/src/tools/web-browsing/Render/Search/index.tsx +2 -2
- package/src/tools/web-browsing/Render/index.tsx +4 -3
- package/src/tools/web-browsing/components/SearchBar.tsx +4 -6
- package/src/tools/web-browsing/index.ts +54 -60
- package/src/tools/web-browsing/systemRole.ts +22 -13
- package/src/types/tool/search/index.ts +44 -0
- package/src/server/routers/tools/__tests__/search.test.ts +0 -48
- 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
|
-
|
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(
|
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
|
|
package/src/server/{routers/tools/__tests__ → services/search/impls/searxng}/fixtures/searXNG.ts
RENAMED
@@ -1,6 +1,6 @@
|
|
1
|
-
import {
|
1
|
+
import { SearXNGSearchResponse } from '../client';
|
2
2
|
|
3
|
-
export const hetongxue:
|
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 './
|
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,
|
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('
|
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('
|
42
|
+
describe('search', () => {
|
43
43
|
it('should handle successful search', async () => {
|
44
|
-
const mockResponse:
|
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
|
-
|
54
|
-
positions: [1],
|
52
|
+
parsedUrl: 'test.com',
|
55
53
|
score: 1,
|
56
|
-
template: 'default',
|
57
54
|
},
|
58
55
|
],
|
59
|
-
|
60
|
-
|
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 {
|
64
|
+
const { search } = result.current;
|
72
65
|
|
73
66
|
const messageId = 'test-message-id';
|
74
67
|
const query: SearchQuery = {
|
75
|
-
|
76
|
-
searchEngines: ['google'],
|
77
|
-
},
|
68
|
+
searchEngines: ['google'],
|
78
69
|
query: 'test query',
|
79
70
|
};
|
80
71
|
|
81
72
|
await act(async () => {
|
82
|
-
await
|
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:
|
95
|
+
const emptyResponse: UniformSearchResponse = {
|
105
96
|
results: [],
|
106
|
-
|
107
|
-
|
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:
|
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
|
-
|
125
|
-
positions: [1],
|
110
|
+
parsedUrl: 'retry.com',
|
126
111
|
score: 1,
|
127
|
-
template: 'default',
|
128
112
|
},
|
129
113
|
],
|
130
|
-
|
131
|
-
|
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 {
|
125
|
+
const { search } = result.current;
|
146
126
|
|
147
127
|
const messageId = 'test-message-id';
|
148
128
|
const query: SearchQuery = {
|
149
|
-
|
150
|
-
|
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
|
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 {
|
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
|
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, '
|
275
|
-
const {
|
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
|
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 {
|
292
|
+
const { saveSearchResult } = result.current;
|
318
293
|
|
319
294
|
await act(async () => {
|
320
|
-
await
|
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 {
|
321
|
+
const { saveSearchResult } = result.current;
|
347
322
|
|
348
323
|
await act(async () => {
|
349
|
-
await
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
130
|
+
|
131
|
+
search: async (id, { query, ...params }, aiSummary = true) => {
|
142
132
|
get().toggleSearchLoading(id, true);
|
143
|
-
let data:
|
133
|
+
let data: UniformSearchResponse | undefined;
|
144
134
|
try {
|
145
135
|
// 首次查询
|
146
|
-
data = await searchService.search(
|
136
|
+
data = await searchService.search(query, params);
|
147
137
|
|
148
138
|
// 如果没有搜索到结果,则执行第一次重试(移除搜索引擎限制)
|
149
139
|
if (
|
150
140
|
data?.results.length === 0 &&
|
151
|
-
params
|
152
|
-
params
|
141
|
+
params?.searchEngines &&
|
142
|
+
params?.searchEngines?.length > 0
|
153
143
|
) {
|
154
144
|
const paramsExcludeSearchEngines = {
|
155
145
|
...params,
|
156
|
-
|
157
|
-
...params.optionalParams,
|
158
|
-
searchEngines: undefined,
|
159
|
-
},
|
146
|
+
searchEngines: undefined,
|
160
147
|
};
|
161
|
-
data = await searchService.search(
|
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(
|
168
|
-
get().updatePluginArguments(id, {
|
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.
|
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
|
});
|
@@ -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
|
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
|
});
|