@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
@@ -1,14 +1,14 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
- import * as withTimeoutModule from '../../utils/withTimeout';
4
3
  import { NetworkConnectionError, PageNotFoundError, TimeoutError } from '../../utils/errorType';
4
+ import * as withTimeoutModule from '../../utils/withTimeout';
5
5
  import { search1api } from '../search1api';
6
6
 
7
7
  describe('search1api crawler', () => {
8
8
  // Mock fetch function
9
9
  const mockFetch = vi.fn();
10
10
  global.fetch = mockFetch;
11
-
11
+
12
12
  // Original env
13
13
  let originalEnv: NodeJS.ProcessEnv;
14
14
 
@@ -16,7 +16,7 @@ describe('search1api crawler', () => {
16
16
  vi.resetAllMocks();
17
17
  originalEnv = { ...process.env };
18
18
  process.env.SEARCH1API_API_KEY = 'test-api-key';
19
-
19
+
20
20
  // Mock withTimeout to directly return the promise
21
21
  vi.spyOn(withTimeoutModule, 'withTimeout').mockImplementation((promise) => promise);
22
22
  });
@@ -25,17 +25,9 @@ describe('search1api crawler', () => {
25
25
  process.env = originalEnv;
26
26
  });
27
27
 
28
- it('should throw error when API key is not set', async () => {
29
- delete process.env.SEARCH1API_API_KEY;
30
-
31
- await expect(search1api('https://example.com', { filterOptions: {} })).rejects.toThrow(
32
- 'SEARCH1API_API_KEY environment variable is not set',
33
- );
34
- });
35
-
36
28
  it('should throw NetworkConnectionError when fetch fails', async () => {
37
29
  mockFetch.mockRejectedValue(new Error('fetch failed'));
38
-
30
+
39
31
  await expect(search1api('https://example.com', { filterOptions: {} })).rejects.toThrow(
40
32
  NetworkConnectionError,
41
33
  );
@@ -44,12 +36,12 @@ describe('search1api crawler', () => {
44
36
  it('should throw TimeoutError when request times out', async () => {
45
37
  // Restore original withTimeout implementation for this test
46
38
  vi.spyOn(withTimeoutModule, 'withTimeout').mockRestore();
47
-
39
+
48
40
  // Mock withTimeout to throw TimeoutError
49
41
  vi.spyOn(withTimeoutModule, 'withTimeout').mockImplementation(() => {
50
42
  throw new TimeoutError('Request timeout after 10000ms');
51
43
  });
52
-
44
+
53
45
  await expect(search1api('https://example.com', { filterOptions: {} })).rejects.toThrow(
54
46
  TimeoutError,
55
47
  );
@@ -61,7 +53,7 @@ describe('search1api crawler', () => {
61
53
  status: 404,
62
54
  statusText: 'Not Found',
63
55
  });
64
-
56
+
65
57
  await expect(search1api('https://example.com', { filterOptions: {} })).rejects.toThrow(
66
58
  PageNotFoundError,
67
59
  );
@@ -73,7 +65,7 @@ describe('search1api crawler', () => {
73
65
  status: 500,
74
66
  statusText: 'Internal Server Error',
75
67
  });
76
-
68
+
77
69
  await expect(search1api('https://example.com', { filterOptions: {} })).rejects.toThrow(
78
70
  'Search1API request failed with status 500: Internal Server Error',
79
71
  );
@@ -82,37 +74,39 @@ describe('search1api crawler', () => {
82
74
  it('should return undefined when content is too short', async () => {
83
75
  mockFetch.mockResolvedValue({
84
76
  ok: true,
85
- json: () => Promise.resolve({
86
- crawlParameters: { url: 'https://example.com' },
87
- results: {
88
- title: 'Test Title',
89
- link: 'https://example.com',
90
- content: 'Short', // Less than 100 characters
91
- },
92
- }),
77
+ json: () =>
78
+ Promise.resolve({
79
+ crawlParameters: { url: 'https://example.com' },
80
+ results: {
81
+ title: 'Test Title',
82
+ link: 'https://example.com',
83
+ content: 'Short', // Less than 100 characters
84
+ },
85
+ }),
93
86
  });
94
-
87
+
95
88
  const result = await search1api('https://example.com', { filterOptions: {} });
96
89
  expect(result).toBeUndefined();
97
90
  });
98
91
 
99
92
  it('should return crawl result on successful fetch', async () => {
100
93
  const mockContent = 'This is a test content that is longer than 100 characters. '.repeat(3);
101
-
94
+
102
95
  mockFetch.mockResolvedValue({
103
96
  ok: true,
104
- json: () => Promise.resolve({
105
- crawlParameters: { url: 'https://example.com' },
106
- results: {
107
- title: 'Test Title',
108
- link: 'https://example.com',
109
- content: mockContent,
110
- },
111
- }),
97
+ json: () =>
98
+ Promise.resolve({
99
+ crawlParameters: { url: 'https://example.com' },
100
+ results: {
101
+ title: 'Test Title',
102
+ link: 'https://example.com',
103
+ content: mockContent,
104
+ },
105
+ }),
112
106
  });
113
-
107
+
114
108
  const result = await search1api('https://example.com', { filterOptions: {} });
115
-
109
+
116
110
  expect(mockFetch).toHaveBeenCalledWith('https://api.search1api.com/crawl', {
117
111
  method: 'POST',
118
112
  headers: {
@@ -123,7 +117,7 @@ describe('search1api crawler', () => {
123
117
  url: 'https://example.com',
124
118
  }),
125
119
  });
126
-
120
+
127
121
  expect(result).toEqual({
128
122
  content: mockContent,
129
123
  contentType: 'text',
@@ -140,8 +134,8 @@ describe('search1api crawler', () => {
140
134
  ok: true,
141
135
  json: () => Promise.reject(new Error('Invalid JSON')),
142
136
  });
143
-
137
+
144
138
  const result = await search1api('https://example.com', { filterOptions: {} });
145
139
  expect(result).toBeUndefined();
146
140
  });
147
- });
141
+ });
@@ -17,12 +17,6 @@ export const search1api: CrawlImpl = async (url) => {
17
17
  // Get API key from environment variable
18
18
  const apiKey = process.env.SEARCH1API_CRAWL_API_KEY || process.env.SEARCH1API_API_KEY;
19
19
 
20
- if (!apiKey) {
21
- throw new Error(
22
- 'SEARCH1API_API_KEY environment variable is not set. Visit https://www.search1api.com to get free quota.',
23
- );
24
- }
25
-
26
20
  let res: Response;
27
21
 
28
22
  try {
@@ -32,7 +26,7 @@ export const search1api: CrawlImpl = async (url) => {
32
26
  url,
33
27
  }),
34
28
  headers: {
35
- 'Authorization': `Bearer ${apiKey}`,
29
+ 'Authorization': !apiKey ? '' : `Bearer ${apiKey}`,
36
30
  'Content-Type': 'application/json',
37
31
  },
38
32
  method: 'POST',
@@ -1,2 +1,3 @@
1
+ export type { CrawlImplType } from './crawImpl';
1
2
  export { Crawler } from './crawler';
2
3
  export * from './type';
@@ -31,6 +31,7 @@ export const crawUrlRules: CrawlUrlRule[] = [
31
31
  filterOptions: {
32
32
  enableReadability: false,
33
33
  },
34
+ impls: ['naive', 'jina'],
34
35
  urlPattern: 'https://github.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)',
35
36
  urlTransform: 'https://github.com/$1/$2/raw/refs/heads/$3/$4',
36
37
  },
@@ -38,6 +39,7 @@ export const crawUrlRules: CrawlUrlRule[] = [
38
39
  filterOptions: {
39
40
  enableReadability: false,
40
41
  },
42
+ impls: ['naive', 'jina'],
41
43
  // GitHub discussion
42
44
  urlPattern: 'https://github.com/(.*)/discussions/(.*)',
43
45
  },
@@ -79,9 +81,9 @@ export const crawUrlRules: CrawlUrlRule[] = [
79
81
  enableReadability: false,
80
82
  pureText: true,
81
83
  },
84
+ impls: ['naive'],
82
85
  urlPattern: 'https://www.qiumiwu.com/standings/(.*)',
83
86
  },
84
-
85
87
  // mozilla use jina
86
88
  {
87
89
  impls: ['jina'],
@@ -6,14 +6,16 @@ const ai21ChatModels: AIChatModelCard[] = [
6
6
  functionCall: true,
7
7
  },
8
8
  contextWindowTokens: 256_000,
9
- displayName: 'Jamba 1.5 Mini',
9
+ description:
10
+ '在同级别中最高效的模型,兼顾速度与质量,具备更小的体积。',
11
+ displayName: 'Jamba Mini',
10
12
  enabled: true,
11
- id: 'jamba-1.5-mini',
13
+ id: 'jamba-mini',
12
14
  pricing: {
13
15
  input: 0.2,
14
16
  output: 0.4,
15
17
  },
16
- releasedAt: '2024-08-22',
18
+ releasedAt: '2025-03-06',
17
19
  type: 'chat',
18
20
  },
19
21
  {
@@ -21,14 +23,16 @@ const ai21ChatModels: AIChatModelCard[] = [
21
23
  functionCall: true,
22
24
  },
23
25
  contextWindowTokens: 256_000,
24
- displayName: 'Jamba 1.5 Large',
26
+ description:
27
+ '我们最强大、最先进的模型,专为处理企业级复杂任务而设计,具备卓越的性能。',
28
+ displayName: 'Jamba Large',
25
29
  enabled: true,
26
- id: 'jamba-1.5-large',
30
+ id: 'jamba-large',
27
31
  pricing: {
28
32
  input: 2,
29
33
  output: 8,
30
34
  },
31
- releasedAt: '2024-08-22',
35
+ releasedAt: '2025-03-06',
32
36
  type: 'chat',
33
37
  },
34
38
  ];
@@ -26,7 +26,6 @@ const ai360ChatModels: AIChatModelCard[] = [
26
26
  description:
27
27
  '360gpt2-o1 使用树搜索构建思维链,并引入了反思机制,使用强化学习训练,模型具备自我反思与纠错的能力。',
28
28
  displayName: '360GPT2 o1',
29
- enabled: true,
30
29
  id: '360gpt2-o1',
31
30
  pricing: {
32
31
  currency: 'CNY',
@@ -36,6 +35,10 @@ const ai360ChatModels: AIChatModelCard[] = [
36
35
  type: 'chat',
37
36
  },
38
37
  {
38
+ abilities: {
39
+ functionCall: true,
40
+ search: true,
41
+ },
39
42
  contextWindowTokens: 8000,
40
43
  description:
41
44
  '360智脑系列效果最好的主力千亿级大模型,广泛适用于各领域复杂任务场景。',
@@ -47,6 +50,9 @@ const ai360ChatModels: AIChatModelCard[] = [
47
50
  input: 2,
48
51
  output: 5,
49
52
  },
53
+ settings: {
54
+ searchImpl: 'params',
55
+ },
50
56
  type: 'chat',
51
57
  },
52
58
  {
@@ -58,7 +64,6 @@ const ai360ChatModels: AIChatModelCard[] = [
58
64
  description:
59
65
  '360智脑系列效果最好的主力千亿级大模型,广泛适用于各领域复杂任务场景。',
60
66
  displayName: '360GPT Pro',
61
- enabled: true,
62
67
  id: '360gpt-pro',
63
68
  pricing: {
64
69
  currency: 'CNY',
@@ -70,6 +75,19 @@ const ai360ChatModels: AIChatModelCard[] = [
70
75
  },
71
76
  type: 'chat',
72
77
  },
78
+ {
79
+ contextWindowTokens: 16_000,
80
+ description:
81
+ '翻译专用模型,深度微调优化,翻译效果领先。',
82
+ displayName: '360GPT Pro Trans',
83
+ id: '360gpt-pro-trans',
84
+ pricing: {
85
+ currency: 'CNY',
86
+ input: 2,
87
+ output: 5,
88
+ },
89
+ type: 'chat',
90
+ },
73
91
  {
74
92
  contextWindowTokens: 7000,
75
93
  description:
@@ -84,6 +102,22 @@ const ai360ChatModels: AIChatModelCard[] = [
84
102
  },
85
103
  type: 'chat',
86
104
  },
105
+ {
106
+ abilities: {
107
+ reasoning: true,
108
+ },
109
+ contextWindowTokens: 64_000,
110
+ description:
111
+ '【360部署版】DeepSeek-R1在后训练阶段大规模使用了强化学习技术,在仅有极少标注数据的情况下,极大提升了模型推理能力。在数学、代码、自然语言推理等任务上,性能比肩 OpenAI o1 正式版。',
112
+ displayName: 'DeepSeek R1',
113
+ id: '360/deepseek-r1',
114
+ pricing: {
115
+ currency: 'CNY',
116
+ input: 4,
117
+ output: 16,
118
+ },
119
+ type: 'chat',
120
+ },
87
121
  ];
88
122
 
89
123
  export const allModels = [...ai360ChatModels];
@@ -13,6 +13,7 @@ const stepfunChatModels: AIChatModelCard[] = [
13
13
  contextWindowTokens: 100_000,
14
14
  description: '该模型是拥有强大的图像理解能力的推理大模型,能够处理图像和文字信息,经过深度思考后输出文本生成文本内容。该模型在视觉推理领域表现突出,同时拥有第一梯队的数学、代码、文本推理能力。上下文长度为100k。',
15
15
  displayName: 'Step R1 V Mini',
16
+ enabled: true,
16
17
  id: 'step-r1-v-mini',
17
18
  pricing: {
18
19
  currency: 'CNY',
@@ -3,6 +3,17 @@ import { AIChatModelCard } from '@/types/aiModel';
3
3
  // https://docs.wair.ac.cn/maas/jiage.html
4
4
 
5
5
  const taichuChatModels: AIChatModelCard[] = [
6
+ {
7
+ abilities: {
8
+ reasoning: true,
9
+ },
10
+ contextWindowTokens: 32_768,
11
+ description: 'taichu_o1是新一代推理大模型,通过多模态交互和强化学习实现类人思维链,支持复杂决策推演,在保持高精度输出的同时展现可模型推理的思维路径,适用于策略分析与深度思考等场景。',
12
+ displayName: 'Taichu O1',
13
+ enabled: true,
14
+ id: 'taichu_o1',
15
+ type: 'chat',
16
+ },
6
17
  {
7
18
  abilities: {
8
19
  functionCall: true,
@@ -35,6 +46,56 @@ const taichuChatModels: AIChatModelCard[] = [
35
46
  },
36
47
  type: 'chat',
37
48
  },
49
+ {
50
+ abilities: {
51
+ reasoning: true,
52
+ },
53
+ contextWindowTokens: 131_072,
54
+ description: 'DeepSeek-R1 是一款强化学习(RL)驱动的推理模型,解决了模型中的重复性和可读性问题。在 RL 之前,DeepSeek-R1 引入了冷启动数据,进一步优化了推理性能。它在数学、代码和推理任务中与 OpenAI-o1 表现相当,并且通过精心设计的训练方法,提升了整体效果。',
55
+ displayName: 'DeepSeek R1',
56
+ id: 'deepseek_r1',
57
+ type: 'chat',
58
+ },
59
+ {
60
+ abilities: {
61
+ reasoning: true,
62
+ },
63
+ contextWindowTokens: 131_072,
64
+ description: 'DeepSeek-R1-Distill-Qwen-14B 是基于 Qwen2.5-14B 通过知识蒸馏得到的模型。该模型使用 DeepSeek-R1 生成的 80 万个精选样本进行微调,展现出优秀的推理能力。',
65
+ displayName: 'DeepSeek R1 Distill Qwen 14B',
66
+ id: 'deepseek_r1_distill_qwen_14b',
67
+ type: 'chat',
68
+ },
69
+ {
70
+ abilities: {
71
+ reasoning: true,
72
+ },
73
+ contextWindowTokens: 131_072,
74
+ description: 'DeepSeek-R1-Distill-Qwen-32B 是基于 Qwen2.5-32B 通过知识蒸馏得到的模型。该模型使用 DeepSeek-R1 生成的 80 万个精选样本进行微调,在数学、编程和推理等多个领域展现出卓越的性能。',
75
+ displayName: 'DeepSeek R1 Distill Qwen 32B',
76
+ id: 'deepseek_r1_distill_qwen_32b',
77
+ type: 'chat',
78
+ },
79
+ {
80
+ abilities: {
81
+ reasoning: true,
82
+ },
83
+ contextWindowTokens: 131_072,
84
+ description: 'DeepSeek-R1-Distill-Llama-70B 是基于 Llama-3.3-70B-Instruct 经过蒸馏训练得到的模型。该模型是 DeepSeek-R1 系列的一部分,通过使用 DeepSeek-R1 生成的样本进行微调,在数学、编程和推理等多个领域展现出优秀的性能。',
85
+ displayName: 'DeepSeek R1 Distill Llama 70B',
86
+ id: 'deepseek_r1_distill_llama_70b',
87
+ type: 'chat',
88
+ },
89
+ {
90
+ abilities: {
91
+ reasoning: true,
92
+ },
93
+ contextWindowTokens: 32_768,
94
+ description: 'Qwen 系列中等规模的推理模型。与传统的指令调优模型相比,具备思考和推理能力的 QwQ 在下游任务中,尤其是在解决难题时,能够显著提升性能。',
95
+ displayName: 'QwQ 32B',
96
+ id: 'qwq_32b',
97
+ type: 'chat',
98
+ },
38
99
  ];
39
100
 
40
101
  export const allModels = [...taichuChatModels];
@@ -61,7 +61,6 @@ const doubaoChatModels: AIChatModelCard[] = [
61
61
  description:
62
62
  'DeepSeek-R1 在后训练阶段大规模使用了强化学习技术,在仅有极少标注数据的情况下,极大提升了模型推理能力。在数学、代码、自然语言推理等任务上,性能比肩 OpenAI o1 正式版。',
63
63
  displayName: 'DeepSeek R1',
64
- enabled: true,
65
64
  id: 'deepseek-r1',
66
65
  maxOutput: 16_384,
67
66
  pricing: {
@@ -26,7 +26,7 @@ const Ai21: ModelProviderCard = {
26
26
  },
27
27
  },
28
28
  ],
29
- checkModel: 'jamba-1.5-mini',
29
+ checkModel: 'jamba-mini',
30
30
  description: 'AI21 Labs 为企业构建基础模型和人工智能系统,加速生成性人工智能在生产中的应用。',
31
31
  id: 'ai21',
32
32
  modelsUrl: 'https://docs.ai21.com/reference',
@@ -5,11 +5,13 @@ export const getToolsConfig = () => {
5
5
  return createEnv({
6
6
  runtimeEnv: {
7
7
  CRAWLER_IMPLS: process.env.CRAWLER_IMPLS,
8
+ SEARCH_PROVIDERS: process.env.SEARCH_PROVIDERS,
8
9
  SEARXNG_URL: process.env.SEARXNG_URL,
9
10
  },
10
11
 
11
12
  server: {
12
13
  CRAWLER_IMPLS: z.string().optional(),
14
+ SEARCH_PROVIDERS: z.string().optional(),
13
15
  SEARXNG_URL: z.string().url().optional(),
14
16
  },
15
17
  });
@@ -300,7 +300,7 @@ describe('AiInfraRepos', () => {
300
300
  });
301
301
 
302
302
  it('should use builtin models', async () => {
303
- const providerId = 'taichu';
303
+ const providerId = 'ai21';
304
304
 
305
305
  vi.spyOn(repo.aiModelModel, 'getModelListByProviderId').mockResolvedValue([]);
306
306
 
@@ -309,8 +309,8 @@ describe('AiInfraRepos', () => {
309
309
  expect(result).toHaveLength(2);
310
310
  expect(result).toEqual(
311
311
  expect.arrayContaining([
312
- expect.objectContaining({ id: 'taichu_llm' }),
313
- expect.objectContaining({ id: 'taichu_vl' }),
312
+ expect.objectContaining({ id: 'jamba-mini' }),
313
+ expect.objectContaining({ id: 'jamba-large' }),
314
314
  ]),
315
315
  );
316
316
  });
@@ -4,6 +4,7 @@ import { memo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import PluginResult from './PluginResultJSON';
7
+ import PluginState from './PluginState';
7
8
 
8
9
  interface DebugProps {
9
10
  payload: object;
@@ -28,15 +29,20 @@ const Debug = memo<DebugProps>(({ payload, requestArgs, toolCallId }) => {
28
29
  key: 'arguments',
29
30
  label: t('debug.arguments'),
30
31
  },
32
+ {
33
+ children: <PluginResult toolCallId={toolCallId} />,
34
+ key: 'response',
35
+ label: t('debug.response'),
36
+ },
31
37
  {
32
38
  children: <Highlighter language={'json'}>{JSON.stringify(payload, null, 2)}</Highlighter>,
33
39
  key: 'function_call',
34
40
  label: t('debug.function_call'),
35
41
  },
36
42
  {
37
- children: <PluginResult toolCallId={toolCallId} />,
38
- key: 'response',
39
- label: t('debug.response'),
43
+ children: <PluginState toolCallId={toolCallId} />,
44
+ key: 'pluginState',
45
+ label: t('debug.pluginState'),
40
46
  },
41
47
  ]}
42
48
  style={{ display: 'grid', maxWidth: 800, minWidth: 400 }}
@@ -0,0 +1,21 @@
1
+ import { Highlighter } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+
4
+ import { useChatStore } from '@/store/chat';
5
+ import { chatSelectors } from '@/store/chat/selectors';
6
+
7
+ export interface FunctionMessageProps {
8
+ toolCallId: string;
9
+ }
10
+
11
+ const PluginState = memo<FunctionMessageProps>(({ toolCallId }) => {
12
+ const toolMessage = useChatStore(chatSelectors.getMessageByToolCallId(toolCallId));
13
+
14
+ return (
15
+ <Highlighter language={'json'} style={{ maxHeight: 200, maxWidth: 800, overflow: 'scroll' }}>
16
+ {JSON.stringify(toolMessage?.pluginState, null, 2)}
17
+ </Highlighter>
18
+ );
19
+ });
20
+
21
+ export default PluginState;
@@ -93,7 +93,7 @@ const ObjectDisplay = memo(({ data, shine }: ObjectDisplayProps) => {
93
93
  <div className={styles.row} key={key}>
94
94
  <span
95
95
  className={styles.key}
96
- style={{ minWidth: hasMinWidth ? (isMobile ? 60 : 80) : undefined }}
96
+ style={{ minWidth: hasMinWidth ? (isMobile ? 60 : 140) : undefined }}
97
97
  >
98
98
  {key}
99
99
  </span>
@@ -5,6 +5,7 @@ export default {
5
5
  off: '关闭调试',
6
6
  on: '查看插件调用信息',
7
7
  payload: '插件载荷',
8
+ pluginState: '插件 State',
8
9
  response: '返回结果',
9
10
  tool_call: '工具调用请求',
10
11
  },
@@ -3,10 +3,10 @@ import { TRPCError } from '@trpc/server';
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { toolsEnv } from '@/config/tools';
6
- import { SearXNGClient } from '@/server/modules/SearXNG';
6
+ import { SearXNGClient } from '@/server/services/search/impls/searxng/client';
7
7
  import { SEARCH_SEARXNG_NOT_CONFIG } from '@/types/tool/search';
8
8
 
9
- import { searchRouter } from '../search';
9
+ import { searchRouter } from './search';
10
10
 
11
11
  // Mock JWT verification
12
12
  vi.mock('@/utils/server/jwt', () => ({
@@ -19,7 +19,7 @@ vi.mock('@lobechat/web-crawler', () => ({
19
19
  })),
20
20
  }));
21
21
 
22
- vi.mock('@/server/modules/SearXNG');
22
+ vi.mock('@/server/services/search/impls/searxng/client');
23
23
 
24
24
  describe('searchRouter', () => {
25
25
  const mockContext = {
@@ -104,7 +104,18 @@ describe('searchRouter', () => {
104
104
  query: 'test query',
105
105
  });
106
106
 
107
- expect(result).toEqual(mockSearchResult);
107
+ expect(result).toEqual({
108
+ costTime: 0,
109
+ query: 'test query',
110
+ results: [
111
+ {
112
+ title: 'Test Result',
113
+ parsedUrl: 'test.com',
114
+ url: 'http://test.com',
115
+ content: 'Test content',
116
+ },
117
+ ],
118
+ });
108
119
  });
109
120
 
110
121
  it('should work without specifying search engines', async () => {
@@ -128,7 +139,18 @@ describe('searchRouter', () => {
128
139
  query: 'test query',
129
140
  });
130
141
 
131
- expect(result).toEqual(mockSearchResult);
142
+ expect(result).toEqual({
143
+ costTime: 0,
144
+ query: 'test query',
145
+ results: [
146
+ {
147
+ title: 'Test Result',
148
+ parsedUrl: 'test.com',
149
+ url: 'http://test.com',
150
+ content: 'Test content',
151
+ },
152
+ ],
153
+ });
132
154
  });
133
155
 
134
156
  it('should handle search errors', async () => {
@@ -1,14 +1,9 @@
1
- import { Crawler } from '@lobechat/web-crawler';
2
- import { TRPCError } from '@trpc/server';
3
- import pMap from 'p-map';
4
1
  import { z } from 'zod';
5
2
 
6
- import { toolsEnv } from '@/config/tools';
7
3
  import { isServerMode } from '@/const/version';
8
4
  import { passwordProcedure } from '@/libs/trpc/edge';
9
5
  import { authedProcedure, router } from '@/libs/trpc/lambda';
10
- import { SearXNGClient } from '@/server/modules/SearXNG';
11
- import { SEARCH_SEARXNG_NOT_CONFIG } from '@/types/tool/search';
6
+ import { searchService } from '@/server/services/search';
12
7
 
13
8
  // TODO: password procedure 未来的处理方式可能要思考下
14
9
  const searchProcedure = isServerMode ? authedProcedure : passwordProcedure;
@@ -22,24 +17,7 @@ export const searchRouter = router({
22
17
  }),
23
18
  )
24
19
  .mutation(async ({ input }) => {
25
- const envString = toolsEnv.CRAWLER_IMPLS || '';
26
-
27
- // 处理全角逗号和多余空格
28
- let envValue = envString.replaceAll(',', ',').trim();
29
-
30
- const impls = envValue.split(',').filter(Boolean);
31
-
32
- const crawler = new Crawler({ impls });
33
-
34
- const results = await pMap(
35
- input.urls,
36
- async (url) => {
37
- return await crawler.crawl({ impls: input.impls, url });
38
- },
39
- { concurrency: 3 },
40
- );
41
-
42
- return { results };
20
+ return searchService.crawlPages(input);
43
21
  }),
44
22
 
45
23
  query: searchProcedure
@@ -56,25 +34,6 @@ export const searchRouter = router({
56
34
  }),
57
35
  )
58
36
  .query(async ({ input }) => {
59
- if (!toolsEnv.SEARXNG_URL) {
60
- throw new TRPCError({ code: 'NOT_IMPLEMENTED', message: SEARCH_SEARXNG_NOT_CONFIG });
61
- }
62
-
63
- const client = new SearXNGClient(toolsEnv.SEARXNG_URL);
64
-
65
- try {
66
- return await client.search(input.query, {
67
- categories: input.optionalParams?.searchCategories,
68
- engines: input.optionalParams?.searchEngines,
69
- time_range: input.optionalParams?.searchTimeRange,
70
- });
71
- } catch (e) {
72
- console.error(e);
73
-
74
- throw new TRPCError({
75
- code: 'SERVICE_UNAVAILABLE',
76
- message: (e as Error).message,
77
- });
78
- }
37
+ return await searchService.query(input.query, input.optionalParams);
79
38
  }),
80
39
  });