@lobehub/chat 1.94.16 → 1.95.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 +50 -0
- package/README.zh-CN.md +8 -8
- package/changelog/v1.json +18 -0
- package/docs/self-hosting/advanced/online-search.mdx +123 -5
- package/docs/self-hosting/advanced/online-search.zh-CN.mdx +123 -4
- package/locales/ar/models.json +2 -2
- package/locales/bg-BG/models.json +2 -2
- package/locales/de-DE/models.json +2 -2
- package/locales/en-US/models.json +2 -2
- package/locales/es-ES/models.json +2 -2
- package/locales/fa-IR/models.json +2 -2
- package/locales/fr-FR/models.json +2 -2
- package/locales/it-IT/models.json +2 -2
- package/locales/ja-JP/models.json +2 -2
- package/locales/ko-KR/models.json +2 -2
- package/locales/nl-NL/models.json +2 -2
- package/locales/pl-PL/models.json +2 -2
- package/locales/pt-BR/models.json +2 -2
- package/locales/ru-RU/models.json +2 -2
- package/locales/tr-TR/models.json +2 -2
- package/locales/vi-VN/models.json +2 -2
- package/locales/zh-CN/models.json +2 -2
- package/locales/zh-TW/models.json +2 -2
- package/package.json +1 -1
- package/src/server/services/search/impls/anspire/index.ts +132 -0
- package/src/server/services/search/impls/anspire/type.ts +21 -0
- package/src/server/services/search/impls/brave/index.ts +129 -0
- package/src/server/services/search/impls/brave/type.ts +58 -0
- package/src/server/services/search/impls/google/index.ts +129 -0
- package/src/server/services/search/impls/google/type.ts +53 -0
- package/src/server/services/search/impls/index.ts +24 -0
- package/src/server/services/search/impls/kagi/index.ts +111 -0
- package/src/server/services/search/impls/kagi/type.ts +24 -0
@@ -1187,8 +1187,8 @@
|
|
1187
1187
|
"google/gemini-2.5-flash-preview:thinking": {
|
1188
1188
|
"description": "Gemini 2.5 Flash — это самая современная основная модель от Google, разработанная для сложного рассуждения, кодирования, математических и научных задач. Она включает встроенную способность \"думать\", что позволяет ей давать ответы с более высокой точностью и детализированной обработкой контекста.\n\nОбратите внимание: эта модель имеет два варианта: с \"думанием\" и без. Цены на вывод значительно различаются в зависимости от того, активирована ли способность думать. Если вы выберете стандартный вариант (без суффикса \":thinking\"), модель явно избегает генерации токенов для размышлений.\n\nЧтобы воспользоваться способностью думать и получать токены для размышлений, вы должны выбрать вариант \":thinking\", что приведет к более высокой цене на вывод размышлений.\n\nКроме того, Gemini 2.5 Flash можно настроить с помощью параметра \"максимальное количество токенов для рассуждения\", как указано в документации (https://openrouter.ai/docs/use-cases/reasoning-tokens#max-tokens-for-reasoning)."
|
1189
1189
|
},
|
1190
|
-
"google/gemini-2.5-pro-preview
|
1191
|
-
"description": "Gemini 2.5 Pro — это самая
|
1190
|
+
"google/gemini-2.5-pro-preview": {
|
1191
|
+
"description": "Gemini 2.5 Pro Preview — это самая передовая модель мышления от Google, способная рассуждать над сложными задачами в области кода, математики и STEM, а также анализировать большие наборы данных, кодовые базы и документы с использованием длинного контекста."
|
1192
1192
|
},
|
1193
1193
|
"google/gemini-flash-1.5": {
|
1194
1194
|
"description": "Gemini 1.5 Flash предлагает оптимизированные возможности многомодальной обработки, подходящие для различных сложных задач."
|
@@ -1187,8 +1187,8 @@
|
|
1187
1187
|
"google/gemini-2.5-flash-preview:thinking": {
|
1188
1188
|
"description": "Gemini 2.5 Flash, Google'ın en gelişmiş ana modelidir ve ileri düzey akıl yürütme, kodlama, matematik ve bilimsel görevler için tasarlanmıştır. Daha yüksek doğruluk ve ayrıntılı bağlam işleme ile yanıtlar sunabilen yerleşik 'düşünme' yeteneğine sahiptir.\n\nNot: Bu modelin iki varyantı vardır: düşünme ve düşünmeme. Çıktı fiyatlandırması, düşünme yeteneğinin etkin olup olmamasına göre önemli ölçüde farklılık gösterir. Standart varyantı (':thinking' eki olmadan) seçerseniz, model açıkça düşünme tokenleri üretmekten kaçınacaktır.\n\nDüşünme yeteneğinden yararlanmak ve düşünme tokenleri almak için, ':thinking' varyantını seçmelisiniz; bu, daha yüksek düşünme çıktı fiyatlandırması ile sonuçlanacaktır.\n\nAyrıca, Gemini 2.5 Flash, belgede belirtildiği gibi 'akıl yürütme maksimum token sayısı' parametresi ile yapılandırılabilir (https://openrouter.ai/docs/use-cases/reasoning-tokens#max-tokens-for-reasoning)."
|
1189
1189
|
},
|
1190
|
-
"google/gemini-2.5-pro-preview
|
1191
|
-
"description": "Gemini 2.5 Pro, Google'ın en gelişmiş
|
1190
|
+
"google/gemini-2.5-pro-preview": {
|
1191
|
+
"description": "Gemini 2.5 Pro Önizlemesi, Google'ın en gelişmiş düşünce modeli olup, kodlama, matematik ve STEM alanlarındaki karmaşık sorunları çözme yeteneğine sahiptir ve uzun bağlam kullanarak büyük veri setleri, kod tabanları ve belgeleri analiz edebilir."
|
1192
1192
|
},
|
1193
1193
|
"google/gemini-flash-1.5": {
|
1194
1194
|
"description": "Gemini 1.5 Flash, optimize edilmiş çok modlu işleme yetenekleri sunar ve çeşitli karmaşık görev senaryolarına uygundur."
|
@@ -1187,8 +1187,8 @@
|
|
1187
1187
|
"google/gemini-2.5-flash-preview:thinking": {
|
1188
1188
|
"description": "Gemini 2.5 Flash là mô hình chủ lực tiên tiến nhất của Google, được thiết kế cho suy luận nâng cao, lập trình, toán học và các nhiệm vụ khoa học. Nó bao gồm khả năng 'suy nghĩ' tích hợp, cho phép nó cung cấp phản hồi với độ chính xác cao hơn và xử lý ngữ cảnh chi tiết hơn.\n\nLưu ý: Mô hình này có hai biến thể: suy nghĩ và không suy nghĩ. Giá đầu ra có sự khác biệt đáng kể tùy thuộc vào việc khả năng suy nghĩ có được kích hoạt hay không. Nếu bạn chọn biến thể tiêu chuẩn (không có hậu tố ':thinking'), mô hình sẽ rõ ràng tránh việc tạo ra các token suy nghĩ.\n\nĐể tận dụng khả năng suy nghĩ và nhận các token suy nghĩ, bạn phải chọn biến thể ':thinking', điều này sẽ tạo ra giá đầu ra suy nghĩ cao hơn.\n\nNgoài ra, Gemini 2.5 Flash có thể được cấu hình thông qua tham số 'số token tối đa cho suy luận', như đã mô tả trong tài liệu (https://openrouter.ai/docs/use-cases/reasoning-tokens#max-tokens-for-reasoning)."
|
1189
1189
|
},
|
1190
|
-
"google/gemini-2.5-pro-preview
|
1191
|
-
"description": "Gemini 2.5 Pro là mô hình
|
1190
|
+
"google/gemini-2.5-pro-preview": {
|
1191
|
+
"description": "Gemini 2.5 Pro Preview là mô hình tư duy tiên tiến nhất của Google, có khả năng suy luận các vấn đề phức tạp trong lĩnh vực mã hóa, toán học và STEM, cũng như phân tích các bộ dữ liệu lớn, kho mã và tài liệu bằng ngữ cảnh dài."
|
1192
1192
|
},
|
1193
1193
|
"google/gemini-flash-1.5": {
|
1194
1194
|
"description": "Gemini 1.5 Flash cung cấp khả năng xử lý đa phương thức được tối ưu hóa, phù hợp cho nhiều tình huống nhiệm vụ phức tạp."
|
@@ -1187,8 +1187,8 @@
|
|
1187
1187
|
"google/gemini-2.5-flash-preview:thinking": {
|
1188
1188
|
"description": "Gemini 2.5 Flash 是 Google 最先进的主力模型,专为高级推理、编码、数学和科学任务而设计。它包含内置的“思考”能力,使其能够提供具有更高准确性和细致上下文处理的响应。\n\n注意:此模型有两个变体:思考和非思考。输出定价根据思考能力是否激活而有显著差异。如果您选择标准变体(不带“:thinking”后缀),模型将明确避免生成思考令牌。\n\n要利用思考能力并接收思考令牌,您必须选择“:thinking”变体,这将产生更高的思考输出定价。\n\n此外,Gemini 2.5 Flash 可通过“推理最大令牌数”参数进行配置,如文档中所述 (https://openrouter.ai/docs/use-cases/reasoning-tokens#max-tokens-for-reasoning)。"
|
1189
1189
|
},
|
1190
|
-
"google/gemini-2.5-pro-preview
|
1191
|
-
"description": "Gemini 2.5 Pro 是 Google
|
1190
|
+
"google/gemini-2.5-pro-preview": {
|
1191
|
+
"description": "Gemini 2.5 Pro Preview 是 Google 最先进的思维模型,能够对代码、数学和STEM领域的复杂问题进行推理,以及使用长上下文分析大型数据集、代码库和文档。"
|
1192
1192
|
},
|
1193
1193
|
"google/gemini-flash-1.5": {
|
1194
1194
|
"description": "Gemini 1.5 Flash 提供了优化后的多模态处理能力,适用多种复杂任务场景。"
|
@@ -1187,8 +1187,8 @@
|
|
1187
1187
|
"google/gemini-2.5-flash-preview:thinking": {
|
1188
1188
|
"description": "Gemini 2.5 Flash 是 Google 最先進的主力模型,專為高級推理、編碼、數學和科學任務而設計。它包含內建的「思考」能力,使其能夠提供具有更高準確性和細緻上下文處理的回應。\n\n注意:此模型有兩個變體:思考和非思考。輸出定價根據思考能力是否啟用而有顯著差異。如果您選擇標準變體(不帶「:thinking」後綴),模型將明確避免生成思考令牌。\n\n要利用思考能力並接收思考令牌,您必須選擇「:thinking」變體,這將產生更高的思考輸出定價。\n\n此外,Gemini 2.5 Flash 可通過「推理最大令牌數」參數進行配置,如文檔中所述 (https://openrouter.ai/docs/use-cases/reasoning-tokens#max-tokens-for-reasoning)。"
|
1189
1189
|
},
|
1190
|
-
"google/gemini-2.5-pro-preview
|
1191
|
-
"description": "Gemini 2.5 Pro 是 Google
|
1190
|
+
"google/gemini-2.5-pro-preview": {
|
1191
|
+
"description": "Gemini 2.5 Pro Preview 是 Google 最先進的思維模型,能夠對程式碼、數學和 STEM 領域的複雜問題進行推理,以及使用長上下文分析大型資料集、程式碼庫和文件。"
|
1192
1192
|
},
|
1193
1193
|
"google/gemini-flash-1.5": {
|
1194
1194
|
"description": "Gemini 1.5 Flash 提供了優化後的多模態處理能力,適用於多種複雜任務場景。"
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.95.0",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -0,0 +1,132 @@
|
|
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 { AnspireSearchParameters, AnspireResponse } from './type';
|
9
|
+
|
10
|
+
const log = debug('lobe-search:Anspire');
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Anspire implementation of the search service
|
14
|
+
* Primarily used for web crawling
|
15
|
+
*/
|
16
|
+
export class AnspireImpl implements SearchServiceImpl {
|
17
|
+
private get apiKey(): string | undefined {
|
18
|
+
return process.env.ANSPIRE_API_KEY;
|
19
|
+
}
|
20
|
+
|
21
|
+
private get baseUrl(): string {
|
22
|
+
// Assuming the base URL is consistent with the crawl endpoint
|
23
|
+
return 'https://plugin.anspire.cn/api';
|
24
|
+
}
|
25
|
+
|
26
|
+
async query(query: string, params: SearchParams = {}): Promise<UniformSearchResponse> {
|
27
|
+
log('Starting Anspire query with query: "%s", params: %o', query, params);
|
28
|
+
const endpoint = urlJoin(this.baseUrl, '/ntsearch/search');
|
29
|
+
|
30
|
+
const defaultQueryParams: AnspireSearchParameters = {
|
31
|
+
mode: 0,
|
32
|
+
query,
|
33
|
+
top_k: 20,
|
34
|
+
};
|
35
|
+
|
36
|
+
let body: AnspireSearchParameters = {
|
37
|
+
...defaultQueryParams,
|
38
|
+
...(params?.searchTimeRange && params.searchTimeRange !== 'anytime'
|
39
|
+
? (() => {
|
40
|
+
const now = Date.now();
|
41
|
+
const days = { day: 1, month: 30, week: 7, year: 365 }[params.searchTimeRange!];
|
42
|
+
|
43
|
+
if (days === undefined) return {};
|
44
|
+
|
45
|
+
return {
|
46
|
+
FromTime: new Date(now - days * 86_400 * 1000).toISOString().slice(0, 19).replace('T', ' '),
|
47
|
+
ToTime: new Date(now).toISOString().slice(0, 19).replace('T', ' '),
|
48
|
+
};
|
49
|
+
})()
|
50
|
+
: {}),
|
51
|
+
};
|
52
|
+
|
53
|
+
log('Constructed request body: %o', body);
|
54
|
+
|
55
|
+
const searchParams = new URLSearchParams();
|
56
|
+
for (const [key, value] of Object.entries(body)) {
|
57
|
+
searchParams.append(key, String(value));
|
58
|
+
}
|
59
|
+
|
60
|
+
let response: Response;
|
61
|
+
const startAt = Date.now();
|
62
|
+
let costTime = 0;
|
63
|
+
try {
|
64
|
+
log('Sending request to endpoint: %s', endpoint);
|
65
|
+
response = await fetch(`${endpoint}?${searchParams.toString()}`, {
|
66
|
+
headers: {
|
67
|
+
'Accept': '*/*',
|
68
|
+
'Authorization': this.apiKey ? `Bearer ${this.apiKey}` : '',
|
69
|
+
'Connection': 'keep-alive ',
|
70
|
+
'Content-Type': 'application/json',
|
71
|
+
},
|
72
|
+
method: 'GET',
|
73
|
+
});
|
74
|
+
log('Received response with status: %d', response.status);
|
75
|
+
costTime = Date.now() - startAt;
|
76
|
+
} catch (error) {
|
77
|
+
log.extend('error')('Anspire fetch error: %o', error);
|
78
|
+
throw new TRPCError({
|
79
|
+
cause: error,
|
80
|
+
code: 'SERVICE_UNAVAILABLE',
|
81
|
+
message: 'Failed to connect to Anspire.',
|
82
|
+
});
|
83
|
+
}
|
84
|
+
|
85
|
+
if (!response.ok) {
|
86
|
+
const errorBody = await response.text();
|
87
|
+
log.extend('error')(
|
88
|
+
`Anspire request failed with status ${response.status}: %s`,
|
89
|
+
errorBody.length > 200 ? `${errorBody.slice(0, 200)}...` : errorBody,
|
90
|
+
);
|
91
|
+
throw new TRPCError({
|
92
|
+
cause: errorBody,
|
93
|
+
code: 'SERVICE_UNAVAILABLE',
|
94
|
+
message: `Anspire request failed: ${response.statusText}`,
|
95
|
+
});
|
96
|
+
}
|
97
|
+
|
98
|
+
try {
|
99
|
+
const anspireResponse = (await response.json()) as AnspireResponse;
|
100
|
+
|
101
|
+
log('Parsed Anspire response: %o', anspireResponse);
|
102
|
+
|
103
|
+
const mappedResults = (anspireResponse.results || []).map(
|
104
|
+
(result): UniformSearchResult => ({
|
105
|
+
category: 'general', // Default category
|
106
|
+
content: result.content || '', // Prioritize content
|
107
|
+
engines: ['anspire'], // Use 'anspire' as the engine name
|
108
|
+
parsedUrl: result.url ? new URL(result.url).hostname : '', // Basic URL parsing
|
109
|
+
score: result.score || 0, // Default score to 0 if undefined
|
110
|
+
title: result.title || '',
|
111
|
+
url: result.url,
|
112
|
+
}),
|
113
|
+
);
|
114
|
+
|
115
|
+
log('Mapped %d results to SearchResult format', mappedResults.length);
|
116
|
+
|
117
|
+
return {
|
118
|
+
costTime,
|
119
|
+
query: query,
|
120
|
+
resultNumbers: mappedResults.length,
|
121
|
+
results: mappedResults,
|
122
|
+
};
|
123
|
+
} catch (error) {
|
124
|
+
log.extend('error')('Error parsing Anspire response: %o', error);
|
125
|
+
throw new TRPCError({
|
126
|
+
cause: error,
|
127
|
+
code: 'INTERNAL_SERVER_ERROR',
|
128
|
+
message: 'Failed to parse Anspire response.',
|
129
|
+
});
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
export interface AnspireSearchParameters {
|
2
|
+
FromTime?: string;
|
3
|
+
Insite?: string;
|
4
|
+
ToTime?: string;
|
5
|
+
mode?: number;
|
6
|
+
query: string;
|
7
|
+
top_k?: number;
|
8
|
+
}
|
9
|
+
|
10
|
+
interface AnspireResults {
|
11
|
+
content?: string;
|
12
|
+
score?: number;
|
13
|
+
title: string;
|
14
|
+
url: string;
|
15
|
+
}
|
16
|
+
|
17
|
+
export interface AnspireResponse {
|
18
|
+
Uuid?: string;
|
19
|
+
query?: string;
|
20
|
+
results?: AnspireResults[];
|
21
|
+
}
|
@@ -0,0 +1,129 @@
|
|
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 { BraveSearchParameters, BraveResponse } from './type';
|
9
|
+
|
10
|
+
const log = debug('lobe-search:Brave');
|
11
|
+
|
12
|
+
const timeRangeMapping = {
|
13
|
+
day: 'pd',
|
14
|
+
month: 'pm',
|
15
|
+
week: 'pw',
|
16
|
+
year: 'py',
|
17
|
+
};
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Brave implementation of the search service
|
21
|
+
* Primarily used for web crawling
|
22
|
+
*/
|
23
|
+
export class BraveImpl implements SearchServiceImpl {
|
24
|
+
private get apiKey(): string | undefined {
|
25
|
+
return process.env.BRAVE_API_KEY;
|
26
|
+
}
|
27
|
+
|
28
|
+
private get baseUrl(): string {
|
29
|
+
// Assuming the base URL is consistent with the crawl endpoint
|
30
|
+
return 'https://api.search.brave.com/res/v1';
|
31
|
+
}
|
32
|
+
|
33
|
+
async query(query: string, params: SearchParams = {}): Promise<UniformSearchResponse> {
|
34
|
+
log('Starting Brave query with query: "%s", params: %o', query, params);
|
35
|
+
const endpoint = urlJoin(this.baseUrl, '/web/search');
|
36
|
+
|
37
|
+
const defaultQueryParams: BraveSearchParameters = {
|
38
|
+
count: 15,
|
39
|
+
q: query,
|
40
|
+
result_filter: 'web',
|
41
|
+
};
|
42
|
+
|
43
|
+
let body: BraveSearchParameters = {
|
44
|
+
...defaultQueryParams,
|
45
|
+
freshness:
|
46
|
+
params?.searchTimeRange && params.searchTimeRange !== 'anytime'
|
47
|
+
? timeRangeMapping[params.searchTimeRange as keyof typeof timeRangeMapping] ?? undefined
|
48
|
+
: undefined,
|
49
|
+
};
|
50
|
+
|
51
|
+
log('Constructed request body: %o', body);
|
52
|
+
|
53
|
+
const searchParams = new URLSearchParams();
|
54
|
+
for (const [key, value] of Object.entries(body)) {
|
55
|
+
searchParams.append(key, String(value));
|
56
|
+
}
|
57
|
+
|
58
|
+
let response: Response;
|
59
|
+
const startAt = Date.now();
|
60
|
+
let costTime = 0;
|
61
|
+
try {
|
62
|
+
log('Sending request to endpoint: %s', endpoint);
|
63
|
+
response = await fetch(`${endpoint}?${searchParams.toString()}`, {
|
64
|
+
headers: {
|
65
|
+
'Accept': 'application/json',
|
66
|
+
'Accept-Encoding': 'gzip',
|
67
|
+
'X-Subscription-Token': this.apiKey ? this.apiKey : '',
|
68
|
+
},
|
69
|
+
method: 'GET',
|
70
|
+
});
|
71
|
+
log('Received response with status: %d', response.status);
|
72
|
+
costTime = Date.now() - startAt;
|
73
|
+
} catch (error) {
|
74
|
+
log.extend('error')('Brave fetch error: %o', error);
|
75
|
+
throw new TRPCError({
|
76
|
+
cause: error,
|
77
|
+
code: 'SERVICE_UNAVAILABLE',
|
78
|
+
message: 'Failed to connect to Brave.',
|
79
|
+
});
|
80
|
+
}
|
81
|
+
|
82
|
+
if (!response.ok) {
|
83
|
+
const errorBody = await response.text();
|
84
|
+
log.extend('error')(
|
85
|
+
`Brave request failed with status ${response.status}: %s`,
|
86
|
+
errorBody.length > 200 ? `${errorBody.slice(0, 200)}...` : errorBody,
|
87
|
+
);
|
88
|
+
throw new TRPCError({
|
89
|
+
cause: errorBody,
|
90
|
+
code: 'SERVICE_UNAVAILABLE',
|
91
|
+
message: `Brave request failed: ${response.statusText}`,
|
92
|
+
});
|
93
|
+
}
|
94
|
+
|
95
|
+
try {
|
96
|
+
const braveResponse = (await response.json()) as BraveResponse;
|
97
|
+
|
98
|
+
log('Parsed Brave response: %o', braveResponse);
|
99
|
+
|
100
|
+
const mappedResults = (braveResponse.web.results || []).map(
|
101
|
+
(result): UniformSearchResult => ({
|
102
|
+
category: 'general', // Default category
|
103
|
+
content: result.description || '', // Prioritize content
|
104
|
+
engines: ['brave'], // Use 'brave' as the engine name
|
105
|
+
parsedUrl: result.url ? new URL(result.url).hostname : '', // Basic URL parsing
|
106
|
+
score: 1, // Default score to 1
|
107
|
+
title: result.title || '',
|
108
|
+
url: result.url,
|
109
|
+
}),
|
110
|
+
);
|
111
|
+
|
112
|
+
log('Mapped %d results to SearchResult format', mappedResults.length);
|
113
|
+
|
114
|
+
return {
|
115
|
+
costTime,
|
116
|
+
query: query,
|
117
|
+
resultNumbers: mappedResults.length,
|
118
|
+
results: mappedResults,
|
119
|
+
};
|
120
|
+
} catch (error) {
|
121
|
+
log.extend('error')('Error parsing Brave response: %o', error);
|
122
|
+
throw new TRPCError({
|
123
|
+
cause: error,
|
124
|
+
code: 'INTERNAL_SERVER_ERROR',
|
125
|
+
message: 'Failed to parse Brave response.',
|
126
|
+
});
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
export interface BraveSearchParameters {
|
2
|
+
count?: number;
|
3
|
+
country?: string;
|
4
|
+
enable_rich_callback?: boolean;
|
5
|
+
extra_snippets?: boolean;
|
6
|
+
freshness?: string;
|
7
|
+
goggles?: string[];
|
8
|
+
goggles_id?: string;
|
9
|
+
offset?: number;
|
10
|
+
q: string;
|
11
|
+
result_filter?: string;
|
12
|
+
safesearch?: string;
|
13
|
+
search_lang?: string;
|
14
|
+
spellcheck?: boolean;
|
15
|
+
summary?: boolean;
|
16
|
+
text_decorations?: boolean;
|
17
|
+
ui_lang?: string;
|
18
|
+
units?: string;
|
19
|
+
}
|
20
|
+
|
21
|
+
interface BraveResults {
|
22
|
+
age?: string;
|
23
|
+
description: string;
|
24
|
+
family_friendly?: boolean;
|
25
|
+
is_live?: boolean;
|
26
|
+
is_source_both?: boolean;
|
27
|
+
is_source_local?: boolean;
|
28
|
+
language?: string;
|
29
|
+
meta_url?: any;
|
30
|
+
page_age?: string;
|
31
|
+
profile?: any;
|
32
|
+
subtype?: string;
|
33
|
+
thumbnail?: any;
|
34
|
+
title: string;
|
35
|
+
type: string;
|
36
|
+
url: string;
|
37
|
+
video?: any;
|
38
|
+
}
|
39
|
+
|
40
|
+
interface BraveVideos {
|
41
|
+
mutated_by_goggles?: boolean;
|
42
|
+
results: BraveResults[];
|
43
|
+
type: string;
|
44
|
+
}
|
45
|
+
|
46
|
+
interface BraveWeb {
|
47
|
+
family_friendly?: boolean;
|
48
|
+
results: BraveResults[];
|
49
|
+
type: string;
|
50
|
+
}
|
51
|
+
|
52
|
+
export interface BraveResponse {
|
53
|
+
mixed: any;
|
54
|
+
query?: any;
|
55
|
+
type: string;
|
56
|
+
videos?: BraveVideos;
|
57
|
+
web: BraveWeb;
|
58
|
+
}
|
@@ -0,0 +1,129 @@
|
|
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 { GoogleSearchParameters, GoogleResponse } from './type';
|
9
|
+
|
10
|
+
const log = debug('lobe-search:Google');
|
11
|
+
|
12
|
+
const timeRangeMapping = {
|
13
|
+
day: 'd1',
|
14
|
+
month: 'm1',
|
15
|
+
week: 'w1',
|
16
|
+
year: 'y1',
|
17
|
+
};
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Google implementation of the search service
|
21
|
+
* Primarily used for web crawling
|
22
|
+
*/
|
23
|
+
export class GoogleImpl implements SearchServiceImpl {
|
24
|
+
private get apiKey(): string | undefined {
|
25
|
+
return process.env.GOOGLE_PSE_API_KEY;
|
26
|
+
}
|
27
|
+
|
28
|
+
private get engineId(): string | undefined {
|
29
|
+
return process.env.GOOGLE_PSE_ENGINE_ID;
|
30
|
+
}
|
31
|
+
|
32
|
+
private get baseUrl(): string {
|
33
|
+
// Assuming the base URL is consistent with the crawl endpoint
|
34
|
+
return 'https://www.googleapis.com';
|
35
|
+
}
|
36
|
+
|
37
|
+
async query(query: string, params: SearchParams = {}): Promise<UniformSearchResponse> {
|
38
|
+
log('Starting Google query with query: "%s", params: %o', query, params);
|
39
|
+
const endpoint = urlJoin(this.baseUrl, '/customsearch/v1');
|
40
|
+
|
41
|
+
const defaultQueryParams: GoogleSearchParameters = {
|
42
|
+
cx: this.engineId || '',
|
43
|
+
key: this.apiKey || '',
|
44
|
+
num: 10,
|
45
|
+
q: query,
|
46
|
+
};
|
47
|
+
|
48
|
+
let body: GoogleSearchParameters = {
|
49
|
+
...defaultQueryParams,
|
50
|
+
dateRestrict:
|
51
|
+
params?.searchTimeRange && params.searchTimeRange !== 'anytime'
|
52
|
+
? timeRangeMapping[params.searchTimeRange as keyof typeof timeRangeMapping] ?? undefined
|
53
|
+
: undefined,
|
54
|
+
};
|
55
|
+
|
56
|
+
log('Constructed request body: %o', body);
|
57
|
+
|
58
|
+
const searchParams = new URLSearchParams();
|
59
|
+
for (const [key, value] of Object.entries(body)) {
|
60
|
+
searchParams.append(key, String(value));
|
61
|
+
}
|
62
|
+
|
63
|
+
let response: Response;
|
64
|
+
const startAt = Date.now();
|
65
|
+
let costTime = 0;
|
66
|
+
try {
|
67
|
+
log('Sending request to endpoint: %s', endpoint);
|
68
|
+
response = await fetch(`${endpoint}?${searchParams.toString()}`, {
|
69
|
+
method: 'GET',
|
70
|
+
});
|
71
|
+
log('Received response with status: %d', response.status);
|
72
|
+
costTime = Date.now() - startAt;
|
73
|
+
} catch (error) {
|
74
|
+
log.extend('error')('Google fetch error: %o', error);
|
75
|
+
throw new TRPCError({
|
76
|
+
cause: error,
|
77
|
+
code: 'SERVICE_UNAVAILABLE',
|
78
|
+
message: 'Failed to connect to Google.',
|
79
|
+
});
|
80
|
+
}
|
81
|
+
|
82
|
+
if (!response.ok) {
|
83
|
+
const errorBody = await response.text();
|
84
|
+
log.extend('error')(
|
85
|
+
`Google request failed with status ${response.status}: %s`,
|
86
|
+
errorBody.length > 200 ? `${errorBody.slice(0, 200)}...` : errorBody,
|
87
|
+
);
|
88
|
+
throw new TRPCError({
|
89
|
+
cause: errorBody,
|
90
|
+
code: 'SERVICE_UNAVAILABLE',
|
91
|
+
message: `Google request failed: ${response.statusText}`,
|
92
|
+
});
|
93
|
+
}
|
94
|
+
|
95
|
+
try {
|
96
|
+
const googleResponse = (await response.json()) as GoogleResponse;
|
97
|
+
|
98
|
+
log('Parsed Google response: %o', googleResponse);
|
99
|
+
|
100
|
+
const mappedResults = (googleResponse.items || []).map(
|
101
|
+
(result): UniformSearchResult => ({
|
102
|
+
category: 'general', // Default category
|
103
|
+
content: result.snippet || '', // Prioritize content
|
104
|
+
engines: ['google'], // Use 'google' as the engine name
|
105
|
+
parsedUrl: result.link ? new URL(result.link).hostname : '', // Basic URL parsing
|
106
|
+
score: 1, // Default score to 1
|
107
|
+
title: result.title || '',
|
108
|
+
url: result.link,
|
109
|
+
}),
|
110
|
+
);
|
111
|
+
|
112
|
+
log('Mapped %d results to SearchResult format', mappedResults.length);
|
113
|
+
|
114
|
+
return {
|
115
|
+
costTime,
|
116
|
+
query: query,
|
117
|
+
resultNumbers: mappedResults.length,
|
118
|
+
results: mappedResults,
|
119
|
+
};
|
120
|
+
} catch (error) {
|
121
|
+
log.extend('error')('Error parsing Google response: %o', error);
|
122
|
+
throw new TRPCError({
|
123
|
+
cause: error,
|
124
|
+
code: 'INTERNAL_SERVER_ERROR',
|
125
|
+
message: 'Failed to parse Google response.',
|
126
|
+
});
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
export interface GoogleSearchParameters {
|
2
|
+
c2coff?: number;
|
3
|
+
cx: string;
|
4
|
+
dateRestrict?: string;
|
5
|
+
exactTerms?: string;
|
6
|
+
excludeTerms?: string;
|
7
|
+
fileType?: string;
|
8
|
+
filter?: string;
|
9
|
+
gl?: string;
|
10
|
+
highRange?: string;
|
11
|
+
hl?: string;
|
12
|
+
hq?: string;
|
13
|
+
imgColorType?: string;
|
14
|
+
imgDominantColor?: string;
|
15
|
+
imgSize?: string;
|
16
|
+
imgType?: string;
|
17
|
+
key: string;
|
18
|
+
linkSite?: string;
|
19
|
+
lowRange?: string;
|
20
|
+
lr?: string;
|
21
|
+
num?: number;
|
22
|
+
orTerms?: string;
|
23
|
+
q: string;
|
24
|
+
rights?: string;
|
25
|
+
safe?: string;
|
26
|
+
searchType?: string;
|
27
|
+
siteSearch?: string;
|
28
|
+
siteSearchFilter?: string;
|
29
|
+
sort?: string;
|
30
|
+
start?: string;
|
31
|
+
}
|
32
|
+
|
33
|
+
interface GoogleItems {
|
34
|
+
displayLink?: string;
|
35
|
+
formattedUrl?: string;
|
36
|
+
htmlFormattedUrl?: string;
|
37
|
+
htmlSnippet?: string;
|
38
|
+
htmlTitle?: string;
|
39
|
+
kind?: string;
|
40
|
+
link: string;
|
41
|
+
pagemap?: any;
|
42
|
+
snippet: string;
|
43
|
+
title: string;
|
44
|
+
}
|
45
|
+
|
46
|
+
export interface GoogleResponse {
|
47
|
+
context?: any;
|
48
|
+
items: GoogleItems[];
|
49
|
+
kind?: string;
|
50
|
+
queries?: any;
|
51
|
+
searchInformation?: any;
|
52
|
+
url?: any;
|
53
|
+
}
|
@@ -1,7 +1,11 @@
|
|
1
|
+
import { AnspireImpl } from './anspire';
|
1
2
|
import { BochaImpl } from './bocha';
|
3
|
+
import { BraveImpl } from './brave';
|
2
4
|
import { ExaImpl } from './exa';
|
3
5
|
import { FirecrawlImpl } from './firecrawl';
|
6
|
+
import { GoogleImpl } from './google';
|
4
7
|
import { JinaImpl } from './jina';
|
8
|
+
import { KagiImpl } from './kagi';
|
5
9
|
import { Search1APIImpl } from './search1api';
|
6
10
|
import { SearXNGImpl } from './searxng';
|
7
11
|
import { TavilyImpl } from './tavily';
|
@@ -12,10 +16,14 @@ import { SearchServiceImpl } from './type';
|
|
12
16
|
* Available search service implementations
|
13
17
|
*/
|
14
18
|
export enum SearchImplType {
|
19
|
+
Anspire = 'anspire',
|
15
20
|
Bocha = 'bocha',
|
21
|
+
Brave = 'brave',
|
16
22
|
Exa = 'exa',
|
17
23
|
Firecrawl = 'firecrawl',
|
24
|
+
Google = 'google',
|
18
25
|
Jina = 'jina',
|
26
|
+
Kagi = 'kagi',
|
19
27
|
SearXNG = 'searxng',
|
20
28
|
Search1API = 'search1api',
|
21
29
|
Tavily = 'tavily',
|
@@ -28,10 +36,18 @@ export const createSearchServiceImpl = (
|
|
28
36
|
type: SearchImplType = SearchImplType.SearXNG,
|
29
37
|
): SearchServiceImpl => {
|
30
38
|
switch (type) {
|
39
|
+
case SearchImplType.Anspire: {
|
40
|
+
return new AnspireImpl();
|
41
|
+
}
|
42
|
+
|
31
43
|
case SearchImplType.Bocha: {
|
32
44
|
return new BochaImpl();
|
33
45
|
}
|
34
46
|
|
47
|
+
case SearchImplType.Brave: {
|
48
|
+
return new BraveImpl();
|
49
|
+
}
|
50
|
+
|
35
51
|
case SearchImplType.Exa: {
|
36
52
|
return new ExaImpl();
|
37
53
|
}
|
@@ -40,10 +56,18 @@ export const createSearchServiceImpl = (
|
|
40
56
|
return new FirecrawlImpl();
|
41
57
|
}
|
42
58
|
|
59
|
+
case SearchImplType.Google: {
|
60
|
+
return new GoogleImpl();
|
61
|
+
}
|
62
|
+
|
43
63
|
case SearchImplType.Jina: {
|
44
64
|
return new JinaImpl();
|
45
65
|
}
|
46
66
|
|
67
|
+
case SearchImplType.Kagi: {
|
68
|
+
return new KagiImpl();
|
69
|
+
}
|
70
|
+
|
47
71
|
case SearchImplType.SearXNG: {
|
48
72
|
return new SearXNGImpl();
|
49
73
|
}
|