@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/Dockerfile +2 -0
  3. package/Dockerfile.database +2 -0
  4. package/Dockerfile.pglite +2 -0
  5. package/changelog/v1.json +21 -0
  6. package/docs/self-hosting/advanced/online-search.mdx +123 -5
  7. package/docs/self-hosting/advanced/online-search.zh-CN.mdx +123 -4
  8. package/locales/ar/setting.json +1 -1
  9. package/locales/bg-BG/setting.json +1 -1
  10. package/locales/de-DE/setting.json +1 -1
  11. package/locales/en-US/setting.json +1 -1
  12. package/locales/es-ES/setting.json +1 -1
  13. package/locales/fa-IR/setting.json +1 -1
  14. package/locales/fr-FR/setting.json +1 -1
  15. package/locales/it-IT/setting.json +1 -1
  16. package/locales/ja-JP/setting.json +1 -1
  17. package/locales/ko-KR/setting.json +1 -1
  18. package/locales/nl-NL/setting.json +1 -1
  19. package/locales/pl-PL/setting.json +1 -1
  20. package/locales/pt-BR/setting.json +1 -1
  21. package/locales/ru-RU/setting.json +1 -1
  22. package/locales/tr-TR/setting.json +1 -1
  23. package/locales/vi-VN/setting.json +1 -1
  24. package/locales/zh-CN/setting.json +1 -1
  25. package/locales/zh-TW/setting.json +1 -1
  26. package/package.json +1 -1
  27. package/src/app/[variants]/(main)/settings/llm/ProviderList/providers.tsx +2 -0
  28. package/src/config/aiModels/index.ts +3 -0
  29. package/src/config/aiModels/v0.ts +63 -0
  30. package/src/config/llm.ts +6 -0
  31. package/src/config/modelProviders/index.ts +4 -0
  32. package/src/config/modelProviders/v0.ts +17 -0
  33. package/src/libs/model-runtime/runtimeMap.ts +2 -0
  34. package/src/libs/model-runtime/types/type.ts +1 -0
  35. package/src/libs/model-runtime/utils/modelParse.ts +6 -0
  36. package/src/libs/model-runtime/v0/index.ts +21 -0
  37. package/src/locales/default/setting.ts +1 -1
  38. package/src/server/services/search/impls/anspire/index.ts +132 -0
  39. package/src/server/services/search/impls/anspire/type.ts +21 -0
  40. package/src/server/services/search/impls/brave/index.ts +129 -0
  41. package/src/server/services/search/impls/brave/type.ts +58 -0
  42. package/src/server/services/search/impls/google/index.ts +129 -0
  43. package/src/server/services/search/impls/google/type.ts +53 -0
  44. package/src/server/services/search/impls/index.ts +24 -0
  45. package/src/server/services/search/impls/kagi/index.ts +111 -0
  46. package/src/server/services/search/impls/kagi/type.ts +24 -0
  47. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +25 -3
  48. package/src/types/user/settings/keyVaults.ts +1 -0
  49. package/src/utils/client/parserPlaceholder.test.ts +0 -21
  50. 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. add systemRole
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
- // 3. handle max_tokens
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
- // 4. handle reasoning_effort
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,设置更高可支持 {{input_template}} 中的 {{date}} 等
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
- // 递归解析,用于处理如 {{input_template}} 存在额外预设变量
129
+ // 递归解析,用于处理如 {{text}} 存在额外预设变量
143
130
  for (let i = 0; i < depth; i++) {
144
131
  try {
145
132
  const variables = Object.fromEntries(