@lobehub/chat 1.94.17 → 1.96.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 +58 -0
- package/Dockerfile +2 -0
- package/Dockerfile.database +2 -0
- package/Dockerfile.pglite +2 -0
- package/changelog/v1.json +21 -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/setting.json +1 -1
- package/locales/bg-BG/setting.json +1 -1
- package/locales/de-DE/setting.json +1 -1
- package/locales/en-US/setting.json +1 -1
- package/locales/es-ES/setting.json +1 -1
- package/locales/fa-IR/setting.json +1 -1
- package/locales/fr-FR/setting.json +1 -1
- package/locales/it-IT/setting.json +1 -1
- package/locales/ja-JP/setting.json +1 -1
- package/locales/ko-KR/setting.json +1 -1
- package/locales/nl-NL/setting.json +1 -1
- package/locales/pl-PL/setting.json +1 -1
- package/locales/pt-BR/setting.json +1 -1
- package/locales/ru-RU/setting.json +1 -1
- package/locales/tr-TR/setting.json +1 -1
- package/locales/vi-VN/setting.json +1 -1
- package/locales/zh-CN/setting.json +1 -1
- package/locales/zh-TW/setting.json +1 -1
- package/package.json +1 -1
- package/src/app/[variants]/(main)/settings/llm/ProviderList/providers.tsx +2 -0
- package/src/config/aiModels/index.ts +3 -0
- package/src/config/aiModels/v0.ts +63 -0
- package/src/config/llm.ts +6 -0
- package/src/config/modelProviders/index.ts +4 -0
- package/src/config/modelProviders/v0.ts +17 -0
- package/src/libs/model-runtime/runtimeMap.ts +2 -0
- package/src/libs/model-runtime/types/type.ts +1 -0
- package/src/libs/model-runtime/utils/modelParse.ts +6 -0
- package/src/libs/model-runtime/v0/index.ts +21 -0
- package/src/locales/default/setting.ts +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
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +25 -3
- package/src/types/user/settings/keyVaults.ts +1 -0
- package/src/utils/client/parserPlaceholder.test.ts +0 -21
- package/src/utils/client/parserPlaceholder.ts +2 -15
@@ -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
|
}
|
@@ -0,0 +1,111 @@
|
|
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 { KagiSearchParameters, KagiResponse } from './type';
|
9
|
+
|
10
|
+
const log = debug('lobe-search:Kagi');
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Kagi implementation of the search service
|
14
|
+
* Primarily used for web crawling
|
15
|
+
*/
|
16
|
+
export class KagiImpl implements SearchServiceImpl {
|
17
|
+
private get apiKey(): string | undefined {
|
18
|
+
return process.env.KAGI_API_KEY;
|
19
|
+
}
|
20
|
+
|
21
|
+
private get baseUrl(): string {
|
22
|
+
// Assuming the base URL is consistent with the crawl endpoint
|
23
|
+
return 'https://kagi.com/api/v0';
|
24
|
+
}
|
25
|
+
|
26
|
+
async query(query: string, params: SearchParams = {}): Promise<UniformSearchResponse> {
|
27
|
+
log('Starting Kagi query with query: "%s", params: %o', query, params);
|
28
|
+
const endpoint = urlJoin(this.baseUrl, '/search');
|
29
|
+
|
30
|
+
const body: KagiSearchParameters = {
|
31
|
+
limit: 15,
|
32
|
+
q: query,
|
33
|
+
};
|
34
|
+
|
35
|
+
log('Constructed request body: %o', body);
|
36
|
+
|
37
|
+
const searchParams = new URLSearchParams();
|
38
|
+
for (const [key, value] of Object.entries(body)) {
|
39
|
+
searchParams.append(key, String(value));
|
40
|
+
}
|
41
|
+
|
42
|
+
let response: Response;
|
43
|
+
const startAt = Date.now();
|
44
|
+
let costTime = 0;
|
45
|
+
try {
|
46
|
+
log('Sending request to endpoint: %s', endpoint);
|
47
|
+
response = await fetch(`${endpoint}?${searchParams.toString()}`, {
|
48
|
+
headers: {
|
49
|
+
'Authorization': this.apiKey ? `Bot ${this.apiKey}` : '',
|
50
|
+
},
|
51
|
+
method: 'GET',
|
52
|
+
});
|
53
|
+
log('Received response with status: %d', response.status);
|
54
|
+
costTime = Date.now() - startAt;
|
55
|
+
} catch (error) {
|
56
|
+
log.extend('error')('Kagi fetch error: %o', error);
|
57
|
+
throw new TRPCError({
|
58
|
+
cause: error,
|
59
|
+
code: 'SERVICE_UNAVAILABLE',
|
60
|
+
message: 'Failed to connect to Kagi.',
|
61
|
+
});
|
62
|
+
}
|
63
|
+
|
64
|
+
if (!response.ok) {
|
65
|
+
const errorBody = await response.text();
|
66
|
+
log.extend('error')(
|
67
|
+
`Kagi request failed with status ${response.status}: %s`,
|
68
|
+
errorBody.length > 200 ? `${errorBody.slice(0, 200)}...` : errorBody,
|
69
|
+
);
|
70
|
+
throw new TRPCError({
|
71
|
+
cause: errorBody,
|
72
|
+
code: 'SERVICE_UNAVAILABLE',
|
73
|
+
message: `Kagi request failed: ${response.statusText}`,
|
74
|
+
});
|
75
|
+
}
|
76
|
+
|
77
|
+
try {
|
78
|
+
const kagiResponse = (await response.json()) as KagiResponse;
|
79
|
+
|
80
|
+
log('Parsed Kagi response: %o', kagiResponse);
|
81
|
+
|
82
|
+
const mappedResults = (kagiResponse.data || []).map(
|
83
|
+
(result): UniformSearchResult => ({
|
84
|
+
category: 'general', // Default category
|
85
|
+
content: result.snippet || '', // Prioritize content
|
86
|
+
engines: ['kagi'], // Use 'kagi' as the engine name
|
87
|
+
parsedUrl: result.url ? new URL(result.url).hostname : '', // Basic URL parsing
|
88
|
+
score: 1, // Default score to 1
|
89
|
+
title: result.title || '',
|
90
|
+
url: result.url,
|
91
|
+
}),
|
92
|
+
);
|
93
|
+
|
94
|
+
log('Mapped %d results to SearchResult format', mappedResults.length);
|
95
|
+
|
96
|
+
return {
|
97
|
+
costTime,
|
98
|
+
query: query,
|
99
|
+
resultNumbers: mappedResults.length,
|
100
|
+
results: mappedResults,
|
101
|
+
};
|
102
|
+
} catch (error) {
|
103
|
+
log.extend('error')('Error parsing Kagi response: %o', error);
|
104
|
+
throw new TRPCError({
|
105
|
+
cause: error,
|
106
|
+
code: 'INTERNAL_SERVER_ERROR',
|
107
|
+
message: 'Failed to parse Kagi response.',
|
108
|
+
});
|
109
|
+
}
|
110
|
+
}
|
111
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
export interface KagiSearchParameters {
|
2
|
+
limit?: number;
|
3
|
+
q: string;
|
4
|
+
}
|
5
|
+
|
6
|
+
interface KagiThumbnail {
|
7
|
+
height?: number | null;
|
8
|
+
url: string;
|
9
|
+
width?: number | null;
|
10
|
+
}
|
11
|
+
|
12
|
+
interface KagiData {
|
13
|
+
published?: number;
|
14
|
+
snippet?: string;
|
15
|
+
t: number;
|
16
|
+
thumbnail?: KagiThumbnail;
|
17
|
+
title: string;
|
18
|
+
url: string;
|
19
|
+
}
|
20
|
+
|
21
|
+
export interface KagiResponse {
|
22
|
+
data: KagiData[];
|
23
|
+
meta?: any;
|
24
|
+
}
|
@@ -1,6 +1,7 @@
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
2
2
|
// Disable the auto sort key eslint rule to make the code more logic and readable
|
3
3
|
import { produce } from 'immer';
|
4
|
+
import { template } from 'lodash-es';
|
4
5
|
import { StateCreator } from 'zustand/vanilla';
|
5
6
|
|
6
7
|
import { LOADING_FLAT, MESSAGE_CANCEL_FLAT } from '@/const/message';
|
@@ -507,6 +508,10 @@ export const generateAIChat: StateCreator<
|
|
507
508
|
const agentConfig = agentSelectors.currentAgentConfig(getAgentStoreState());
|
508
509
|
const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
|
509
510
|
|
511
|
+
const compiler = template(chatConfig.inputTemplate, {
|
512
|
+
interpolate: /{{\s*(text)\s*}}/g
|
513
|
+
});
|
514
|
+
|
510
515
|
// ================================== //
|
511
516
|
// messages uniformly preprocess //
|
512
517
|
// ================================== //
|
@@ -521,17 +526,34 @@ export const generateAIChat: StateCreator<
|
|
521
526
|
historyCount,
|
522
527
|
});
|
523
528
|
|
524
|
-
// 2.
|
529
|
+
// 2. replace inputMessage template
|
530
|
+
preprocessMsgs = !chatConfig.inputTemplate
|
531
|
+
? preprocessMsgs
|
532
|
+
: preprocessMsgs.map((m) => {
|
533
|
+
if (m.role === 'user') {
|
534
|
+
try {
|
535
|
+
return { ...m, content: compiler({ text: m.content }) };
|
536
|
+
} catch (error) {
|
537
|
+
console.error(error);
|
538
|
+
|
539
|
+
return m;
|
540
|
+
}
|
541
|
+
}
|
542
|
+
|
543
|
+
return m;
|
544
|
+
});
|
545
|
+
|
546
|
+
// 3. add systemRole
|
525
547
|
if (agentConfig.systemRole) {
|
526
548
|
preprocessMsgs.unshift({ content: agentConfig.systemRole, role: 'system' } as ChatMessage);
|
527
549
|
}
|
528
550
|
|
529
|
-
//
|
551
|
+
// 4. handle max_tokens
|
530
552
|
agentConfig.params.max_tokens = chatConfig.enableMaxTokens
|
531
553
|
? agentConfig.params.max_tokens
|
532
554
|
: undefined;
|
533
555
|
|
534
|
-
//
|
556
|
+
// 5. handle reasoning_effort
|
535
557
|
agentConfig.params.reasoning_effort = chatConfig.enableReasoningEffort
|
536
558
|
? agentConfig.params.reasoning_effort
|
537
559
|
: undefined;
|
@@ -80,6 +80,7 @@ export interface UserKeyVaults extends SearchEngineKeyVaults {
|
|
80
80
|
tencentcloud?: OpenAICompatibleKeyVault;
|
81
81
|
togetherai?: OpenAICompatibleKeyVault;
|
82
82
|
upstage?: OpenAICompatibleKeyVault;
|
83
|
+
v0?: OpenAICompatibleKeyVault;
|
83
84
|
vertexai?: OpenAICompatibleKeyVault;
|
84
85
|
vllm?: OpenAICompatibleKeyVault;
|
85
86
|
volcengine?: OpenAICompatibleKeyVault;
|
@@ -21,18 +21,6 @@ vi.mock('@/store/user/selectors', () => ({
|
|
21
21
|
},
|
22
22
|
}));
|
23
23
|
|
24
|
-
vi.mock('@/store/agent/store', () => ({
|
25
|
-
getAgentStoreState: () => ({}),
|
26
|
-
}));
|
27
|
-
|
28
|
-
vi.mock('@/store/agent/selectors', () => ({
|
29
|
-
agentChatConfigSelectors: {
|
30
|
-
currentChatConfig: () => ({
|
31
|
-
inputTemplate: 'Hello {{username}}!',
|
32
|
-
}),
|
33
|
-
},
|
34
|
-
}));
|
35
|
-
|
36
24
|
describe('parsePlaceholderVariablesMessages', () => {
|
37
25
|
beforeEach(() => {
|
38
26
|
// Mock Date for consistent testing
|
@@ -238,15 +226,6 @@ describe('parsePlaceholderVariablesMessages', () => {
|
|
238
226
|
// Unknown variables should remain unchanged
|
239
227
|
expect(result[0].content).toBe('Hello {{unknown_variable}}!');
|
240
228
|
});
|
241
|
-
|
242
|
-
it('should handle nested variables (input_template)', () => {
|
243
|
-
const messages = [{ id: '1', content: 'Template: {{input_template}}' }];
|
244
|
-
|
245
|
-
const result = parsePlaceholderVariablesMessages(messages);
|
246
|
-
|
247
|
-
// Should resolve nested variables in input_template
|
248
|
-
expect(result[0].content).toBe('Template: Hello testuser!');
|
249
|
-
});
|
250
229
|
});
|
251
230
|
|
252
231
|
describe('specific variable types', () => {
|
@@ -5,9 +5,6 @@ import { uuid } from '@/utils/uuid';
|
|
5
5
|
import { useUserStore } from '@/store/user';
|
6
6
|
import { userProfileSelectors } from '@/store/user/selectors';
|
7
7
|
|
8
|
-
import { getAgentStoreState } from '@/store/agent/store';
|
9
|
-
import { agentChatConfigSelectors } from '@/store/agent/selectors';
|
10
|
-
|
11
8
|
const placeholderVariablesRegex = /{{(.*?)}}/g;
|
12
9
|
|
13
10
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
@@ -108,16 +105,6 @@ export const VARIABLE_GENERATORS = {
|
|
108
105
|
language: () => typeof navigator !== 'undefined' ? navigator.language : '',
|
109
106
|
platform: () => typeof navigator !== 'undefined' ? navigator.platform : '',
|
110
107
|
user_agent: () => typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
111
|
-
|
112
|
-
/**
|
113
|
-
* LobeChat 模板变量
|
114
|
-
*
|
115
|
-
* | Value | Example |
|
116
|
-
* |-------|---------|
|
117
|
-
* | `{{input_template}}` | Some contents |
|
118
|
-
*
|
119
|
-
*/
|
120
|
-
input_template: () => agentChatConfigSelectors.currentChatConfig(getAgentStoreState()).inputTemplate || '',
|
121
108
|
} as Record<string, () => string>;
|
122
109
|
|
123
110
|
/**
|
@@ -133,13 +120,13 @@ const extractPlaceholderVariables = (text: string): string[] => {
|
|
133
120
|
/**
|
134
121
|
* 将模板变量替换为实际值,并支持递归解析嵌套变量
|
135
122
|
* @param text - 含变量的原始文本
|
136
|
-
* @param depth - 递归深度,默认 1,设置更高可支持 {{
|
123
|
+
* @param depth - 递归深度,默认 1,设置更高可支持 {{text}} 中的 {{date}} 等
|
137
124
|
* @returns 替换后的文本
|
138
125
|
*/
|
139
126
|
export const parsePlaceholderVariables = (text: string, depth = 2): string => {
|
140
127
|
let result = text;
|
141
128
|
|
142
|
-
// 递归解析,用于处理如 {{
|
129
|
+
// 递归解析,用于处理如 {{text}} 存在额外预设变量
|
143
130
|
for (let i = 0; i < depth; i++) {
|
144
131
|
try {
|
145
132
|
const variables = Object.fromEntries(
|