@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
@@ -0,0 +1,30 @@
1
+ import { Search1APIImpl } from './search1api';
2
+ import { SearXNGImpl } from './searxng';
3
+ import { SearchServiceImpl } from './type';
4
+
5
+ /**
6
+ * Available search service implementations
7
+ */
8
+ export enum SearchImplType {
9
+ SearXNG = 'searxng',
10
+ Search1API = 'search1api',
11
+ }
12
+
13
+ /**
14
+ * Create a search service implementation instance
15
+ */
16
+ export const createSearchServiceImpl = (
17
+ type: SearchImplType = SearchImplType.SearXNG,
18
+ ): SearchServiceImpl => {
19
+ switch (type) {
20
+ case SearchImplType.SearXNG: {
21
+ return new SearXNGImpl();
22
+ }
23
+
24
+ default: {
25
+ return new Search1APIImpl();
26
+ }
27
+ }
28
+ };
29
+
30
+ export type { SearchServiceImpl } from './type';
@@ -0,0 +1,154 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import debug from 'debug';
3
+ import urlJoin from 'url-join';
4
+
5
+ import { SearchParams, UniformSearchResponse, UniformSearchResult } from '@/types/tool/search';
6
+
7
+ import { SearchServiceImpl } from '../type';
8
+ import { Search1ApiResponse } from './type';
9
+
10
+ interface Search1APIQueryParams {
11
+ crawl_results?: 0 | 1;
12
+ exclude_sites?: string[];
13
+ image?: boolean;
14
+ include_sites?: string[];
15
+ language?: string;
16
+ max_results: number;
17
+ query: string;
18
+ search_service?: string;
19
+ time_range?: string;
20
+ }
21
+
22
+ const log = debug('lobe-search:search1api');
23
+
24
+ /**
25
+ * Search1API implementation of the search service
26
+ * Primarily used for web crawling
27
+ */
28
+ export class Search1APIImpl implements SearchServiceImpl {
29
+ private get apiKey(): string | undefined {
30
+ return process.env.SEARCH1API_SEARCH_API_KEY || process.env.SEARCH1API_API_KEY;
31
+ }
32
+
33
+ private get baseUrl(): string {
34
+ // Assuming the base URL is consistent with the crawl endpoint
35
+ return 'https://api.search1api.com';
36
+ }
37
+
38
+ async query(query: string, params: SearchParams = {}): Promise<UniformSearchResponse> {
39
+ log('Starting Search1API query with query: "%s", params: %o', query, params);
40
+ const endpoint = urlJoin(this.baseUrl, '/search');
41
+
42
+ const { searchEngines } = params;
43
+
44
+ const defaultQueryParams: Search1APIQueryParams = {
45
+ crawl_results: 0, // 默认不做抓取
46
+ image: false,
47
+ max_results: 15, // Default max results
48
+ query,
49
+ };
50
+
51
+ let body: Search1APIQueryParams[] = [
52
+ {
53
+ ...defaultQueryParams,
54
+ time_range:
55
+ params?.searchTimeRange && params.searchTimeRange !== 'anytime'
56
+ ? params.searchTimeRange
57
+ : undefined,
58
+ },
59
+ ];
60
+
61
+ if (searchEngines && searchEngines.length > 0) {
62
+ body = searchEngines.map((searchEngine) => ({
63
+ ...defaultQueryParams,
64
+
65
+ max_results: parseInt((20 / searchEngines.length).toFixed(0)),
66
+ search_service: searchEngine,
67
+ time_range:
68
+ params?.searchTimeRange && params.searchTimeRange !== 'anytime'
69
+ ? params.searchTimeRange
70
+ : undefined,
71
+ }));
72
+ }
73
+
74
+ // Note: Other SearchParams like searchCategories, searchEngines (beyond the first one)
75
+ // and Search1API specific params like include_sites, exclude_sites, language
76
+ // are not currently mapped.
77
+
78
+ log('Constructed request body: %o', body);
79
+
80
+ let response: Response;
81
+ const startAt = Date.now();
82
+ let costTime = 0;
83
+ try {
84
+ log('Sending request to endpoint: %s', endpoint);
85
+ response = await fetch(endpoint, {
86
+ body: JSON.stringify(body),
87
+ headers: {
88
+ 'Authorization': this.apiKey ? `Bearer ${this.apiKey}` : '',
89
+ 'Content-Type': 'application/json',
90
+ },
91
+ method: 'POST',
92
+ });
93
+ log('Received response with status: %d', response.status);
94
+ costTime = Date.now() - startAt;
95
+ } catch (error) {
96
+ log.extend('error')('Search1API fetch error: %o', error);
97
+ throw new TRPCError({
98
+ cause: error,
99
+ code: 'SERVICE_UNAVAILABLE',
100
+ message: 'Failed to connect to Search1API.',
101
+ });
102
+ }
103
+
104
+ if (!response.ok) {
105
+ const errorBody = await response.text();
106
+ log.extend('error')(
107
+ `Search1API request failed with status ${response.status}: %s`,
108
+ errorBody.length > 200 ? `${errorBody.slice(0, 200)}...` : errorBody,
109
+ );
110
+ throw new TRPCError({
111
+ cause: errorBody,
112
+ code: 'SERVICE_UNAVAILABLE',
113
+ message: `Search1API request failed: ${response.statusText}`,
114
+ });
115
+ }
116
+
117
+ try {
118
+ const search1ApiResponse = (await response.json()) as Search1ApiResponse[]; // Use a specific type if defined elsewhere
119
+
120
+ log('Parsed Search1API response: %o', search1ApiResponse);
121
+
122
+ const mappedResults = search1ApiResponse.flatMap((response) => {
123
+ // Map Search1API response to SearchResponse
124
+ return (response.results || []).map(
125
+ (result): UniformSearchResult => ({
126
+ category: 'general', // Default category
127
+ content: result.content || result.snippet || '', // Prioritize content, fallback to snippet
128
+ engines: [response.searchParameters?.search_service || ''],
129
+ parsedUrl: result.link ? new URL(result.link).hostname : '', // Basic URL parsing
130
+ score: 1, // Default score
131
+ title: result.title || '',
132
+ url: result.link,
133
+ }),
134
+ );
135
+ });
136
+
137
+ log('Mapped %d results to SearchResult format', mappedResults.length);
138
+
139
+ return {
140
+ costTime,
141
+ query: query,
142
+ resultNumbers: mappedResults.length,
143
+ results: mappedResults,
144
+ };
145
+ } catch (error) {
146
+ log.extend('error')('Error parsing Search1API response: %o', error);
147
+ throw new TRPCError({
148
+ cause: error,
149
+ code: 'INTERNAL_SERVER_ERROR',
150
+ message: 'Failed to parse Search1API response.',
151
+ });
152
+ }
153
+ }
154
+ }
@@ -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