@lobehub/chat 1.80.5 → 1.81.1

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 (56) 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/sensenova.ts +120 -5
  16. package/src/config/tools.ts +2 -0
  17. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/Debug.tsx +9 -3
  18. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +21 -0
  19. package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments.tsx +1 -1
  20. package/src/libs/agent-runtime/sensenova/index.ts +17 -4
  21. package/src/libs/agent-runtime/utils/sensenovaHelpers.test.ts +108 -0
  22. package/src/libs/agent-runtime/utils/sensenovaHelpers.ts +30 -0
  23. package/src/locales/default/plugin.ts +1 -0
  24. package/src/server/routers/tools/{__test__/search.test.ts → search.test.ts} +27 -5
  25. package/src/server/routers/tools/search.ts +3 -44
  26. package/src/server/services/search/impls/index.ts +30 -0
  27. package/src/server/services/search/impls/search1api/index.ts +154 -0
  28. package/src/server/services/search/impls/search1api/type.ts +81 -0
  29. package/src/server/{modules/SearXNG.ts → services/search/impls/searxng/client.ts} +32 -2
  30. package/src/server/{routers/tools/__tests__ → services/search/impls/searxng}/fixtures/searXNG.ts +2 -2
  31. package/src/server/services/search/impls/searxng/index.test.ts +26 -0
  32. package/src/server/services/search/impls/searxng/index.ts +62 -0
  33. package/src/server/services/search/impls/type.ts +11 -0
  34. package/src/server/services/search/index.ts +59 -0
  35. package/src/store/chat/slices/builtinTool/actions/index.ts +1 -1
  36. package/src/store/chat/slices/builtinTool/actions/{searXNG.test.ts → search.test.ts} +30 -55
  37. package/src/store/chat/slices/builtinTool/actions/{searXNG.ts → search.ts} +25 -32
  38. package/src/tools/web-browsing/Portal/Search/Footer.tsx +1 -1
  39. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/TitleExtra.tsx +2 -2
  40. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/Video.tsx +9 -7
  41. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/index.tsx +2 -2
  42. package/src/tools/web-browsing/Portal/Search/ResultList/index.tsx +3 -3
  43. package/src/tools/web-browsing/Portal/Search/index.tsx +4 -4
  44. package/src/tools/web-browsing/Portal/index.tsx +3 -1
  45. package/src/tools/web-browsing/Render/Search/SearchQuery/SearchView.tsx +4 -2
  46. package/src/tools/web-browsing/Render/Search/SearchQuery/index.tsx +6 -13
  47. package/src/tools/web-browsing/Render/Search/SearchResult/SearchResultItem.tsx +2 -2
  48. package/src/tools/web-browsing/Render/Search/SearchResult/index.tsx +5 -5
  49. package/src/tools/web-browsing/Render/Search/index.tsx +2 -2
  50. package/src/tools/web-browsing/Render/index.tsx +4 -3
  51. package/src/tools/web-browsing/components/SearchBar.tsx +4 -6
  52. package/src/tools/web-browsing/index.ts +54 -60
  53. package/src/tools/web-browsing/systemRole.ts +22 -13
  54. package/src/types/tool/search/index.ts +44 -0
  55. package/src/server/routers/tools/__tests__/search.test.ts +0 -48
  56. 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'],
@@ -4,6 +4,62 @@ import { AIChatModelCard } from '@/types/aiModel';
4
4
  // https://www.sensecore.cn/help/docs/model-as-a-service/nova/release
5
5
 
6
6
  const sensenovaChatModels: AIChatModelCard[] = [
7
+ {
8
+ abilities: {
9
+ reasoning: true,
10
+ vision: true,
11
+ },
12
+ contextWindowTokens: 131_072,
13
+ description:
14
+ '兼顾视觉、语言深度推理,实现慢思考和深度推理,呈现完整的思维链过程。',
15
+ displayName: 'SenseNova V6 Reasoner',
16
+ enabled: true,
17
+ id: 'SenseNova-V6-Reasoner',
18
+ pricing: {
19
+ currency: 'CNY',
20
+ input: 4,
21
+ output: 16,
22
+ },
23
+ releasedAt: '2025-04-14',
24
+ type: 'chat',
25
+ },
26
+ {
27
+ abilities: {
28
+ reasoning: true,
29
+ vision: true,
30
+ },
31
+ contextWindowTokens: 131_072,
32
+ description:
33
+ '实现图片、文本、视频能力的原生统一,突破传统多模态分立局限,在多模基础能力、语言基础能力等核心维度全面领先,文理兼修,在多项测评中多次位列国内外第一梯队水平。',
34
+ displayName: 'SenseNova V6 Turbo',
35
+ enabled: true,
36
+ id: 'SenseNova-V6-Turbo',
37
+ pricing: {
38
+ currency: 'CNY',
39
+ input: 1.5,
40
+ output: 4.5,
41
+ },
42
+ releasedAt: '2025-04-14',
43
+ type: 'chat',
44
+ },
45
+ {
46
+ abilities: {
47
+ vision: true,
48
+ },
49
+ contextWindowTokens: 131_072,
50
+ description:
51
+ '实现图片、文本、视频能力的原生统一,突破传统多模态分立局限,在OpenCompass和SuperCLUE评测中斩获双冠军。',
52
+ displayName: 'SenseNova V6 Pro',
53
+ enabled: true,
54
+ id: 'SenseNova-V6-Pro',
55
+ pricing: {
56
+ currency: 'CNY',
57
+ input: 9,
58
+ output: 3,
59
+ },
60
+ releasedAt: '2025-04-14',
61
+ type: 'chat',
62
+ },
7
63
  {
8
64
  abilities: {
9
65
  functionCall: true,
@@ -12,7 +68,6 @@ const sensenovaChatModels: AIChatModelCard[] = [
12
68
  description:
13
69
  '是基于V5.5的最新版本,较上版本在中英文基础能力,聊天,理科知识, 文科知识,写作,数理逻辑,字数控制 等几个维度的表现有显著提升。',
14
70
  displayName: 'SenseChat 5.5 1202',
15
- enabled: true,
16
71
  id: 'SenseChat-5-1202',
17
72
  pricing: {
18
73
  currency: 'CNY',
@@ -30,7 +85,6 @@ const sensenovaChatModels: AIChatModelCard[] = [
30
85
  description:
31
86
  '是最新的轻量版本模型,达到全量模型90%以上能力,显著降低推理成本。',
32
87
  displayName: 'SenseChat Turbo 1202',
33
- enabled: true,
34
88
  id: 'SenseChat-Turbo-1202',
35
89
  pricing: {
36
90
  currency: 'CNY',
@@ -48,7 +102,6 @@ const sensenovaChatModels: AIChatModelCard[] = [
48
102
  description:
49
103
  '最新版本模型 (V5.5),128K上下文长度,在数学推理、英文对话、指令跟随以及长文本理解等领域能力显著提升,比肩GPT-4o。',
50
104
  displayName: 'SenseChat 5.5',
51
- enabled: true,
52
105
  id: 'SenseChat-5',
53
106
  pricing: {
54
107
  currency: 'CNY',
@@ -58,10 +111,12 @@ const sensenovaChatModels: AIChatModelCard[] = [
58
111
  type: 'chat',
59
112
  },
60
113
  {
114
+ abilities: {
115
+ vision: true,
116
+ },
61
117
  contextWindowTokens: 32_768,
62
118
  description: '最新版本模型 (V5.5),支持多图的输入,全面实现模型基础能力优化,在对象属性识别、空间关系、动作事件识别、场景理解、情感识别、逻辑常识推理和文本理解生成上都实现了较大提升。',
63
119
  displayName: 'SenseChat 5.5 Vision',
64
- enabled: true,
65
120
  id: 'SenseChat-Vision',
66
121
  pricing: {
67
122
  currency: 'CNY',
@@ -78,7 +133,6 @@ const sensenovaChatModels: AIChatModelCard[] = [
78
133
  contextWindowTokens: 32_768,
79
134
  description: '适用于快速问答、模型微调场景',
80
135
  displayName: 'SenseChat 5.0 Turbo',
81
- enabled: true,
82
136
  id: 'SenseChat-Turbo',
83
137
  pricing: {
84
138
  currency: 'CNY',
@@ -160,6 +214,67 @@ const sensenovaChatModels: AIChatModelCard[] = [
160
214
  },
161
215
  type: 'chat',
162
216
  },
217
+ {
218
+ contextWindowTokens: 32_768,
219
+ description:
220
+ 'DeepSeek-V3 是一款由深度求索公司自研的MoE模型。DeepSeek-V3 多项评测成绩超越了 Qwen2.5-72B 和 Llama-3.1-405B 等其他开源模型,并在性能上和世界顶尖的闭源模型 GPT-4o 以及 Claude-3.5-Sonnet 不分伯仲。',
221
+ displayName: 'DeepSeek V3',
222
+ id: 'DeepSeek-V3',
223
+ pricing: {
224
+ currency: 'CNY',
225
+ input: 2,
226
+ output: 8,
227
+ },
228
+ type: 'chat',
229
+ },
230
+ {
231
+ abilities: {
232
+ reasoning: true,
233
+ },
234
+ contextWindowTokens: 32_768,
235
+ description:
236
+ 'DeepSeek-R1 在后训练阶段大规模使用了强化学习技术,在仅有极少标注数据的情况下,极大提升了模型推理能力。在数学、代码、自然语言推理等任务上,性能比肩 OpenAI o1 正式版。',
237
+ displayName: 'DeepSeek R1',
238
+ id: 'DeepSeek-R1',
239
+ pricing: {
240
+ currency: 'CNY',
241
+ input: 4,
242
+ output: 16,
243
+ },
244
+ type: 'chat',
245
+ },
246
+ {
247
+ abilities: {
248
+ reasoning: true,
249
+ },
250
+ contextWindowTokens: 32_768,
251
+ description:
252
+ 'DeepSeek-R1-Distill 模型是在开源模型的基础上通过微调训练得到的,训练过程中使用了由 DeepSeek-R1 生成的样本数据。',
253
+ displayName: 'DeepSeek R1 Distill Qwen 14B',
254
+ id: 'DeepSeek-R1-Distill-Qwen-14B',
255
+ pricing: {
256
+ currency: 'CNY',
257
+ input: 0,
258
+ output: 0,
259
+ },
260
+ type: 'chat',
261
+ },
262
+ {
263
+ abilities: {
264
+ reasoning: true,
265
+ },
266
+ contextWindowTokens: 8192,
267
+ description:
268
+ 'DeepSeek-R1-Distill 模型是在开源模型的基础上通过微调训练得到的,训练过程中使用了由 DeepSeek-R1 生成的样本数据。',
269
+ displayName: 'DeepSeek R1 Distill Qwen 32B',
270
+ id: 'DeepSeek-R1-Distill-Qwen-32B',
271
+ pricing: {
272
+ currency: 'CNY',
273
+ input: 0,
274
+ output: 0,
275
+ },
276
+ type: 'chat',
277
+ },
163
278
  ];
164
279
 
165
280
  export const allModels = [...sensenovaChatModels];
@@ -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
  });
@@ -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>
@@ -1,6 +1,8 @@
1
1
  import { ModelProvider } from '../types';
2
2
  import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
3
3
 
4
+ import { convertSenseNovaMessage } from '../utils/sensenovaHelpers';
5
+
4
6
  import type { ChatModelCard } from '@/types/llm';
5
7
 
6
8
  export interface SenseNovaModelCard {
@@ -11,7 +13,7 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
11
13
  baseURL: 'https://api.sensenova.cn/compatible-mode/v1',
12
14
  chatCompletion: {
13
15
  handlePayload: (payload) => {
14
- const { frequency_penalty, temperature, top_p, ...rest } = payload;
16
+ const { frequency_penalty, messages, model, temperature, top_p, ...rest } = payload;
15
17
 
16
18
  return {
17
19
  ...rest,
@@ -19,6 +21,12 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
19
21
  frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 2
20
22
  ? frequency_penalty
21
23
  : undefined,
24
+ messages: messages.map((message) =>
25
+ message.role !== 'user' || !/^Sense(Nova-V6|Chat-Vision)/.test(model)
26
+ ? message
27
+ : { ...message, content: convertSenseNovaMessage(message.content) }
28
+ ) as any[],
29
+ model,
22
30
  stream: true,
23
31
  temperature:
24
32
  temperature !== undefined && temperature > 0 && temperature <= 2
@@ -35,12 +43,17 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
35
43
  const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
36
44
 
37
45
  const functionCallKeywords = [
38
- 'deepseek-v3',
39
46
  'sensechat-5',
40
47
  ];
41
48
 
49
+ const visionKeywords = [
50
+ 'vision',
51
+ 'sensenova-v6',
52
+ ];
53
+
42
54
  const reasoningKeywords = [
43
- 'deepseek-r1'
55
+ 'deepseek-r1',
56
+ 'sensenova-v6',
44
57
  ];
45
58
 
46
59
  client.baseURL = 'https://api.sensenova.cn/v1/llm';
@@ -66,7 +79,7 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
66
79
  || knownModel?.abilities?.reasoning
67
80
  || false,
68
81
  vision:
69
- model.id.toLowerCase().includes('vision')
82
+ visionKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
70
83
  || knownModel?.abilities?.vision
71
84
  || false,
72
85
  };
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { convertSenseNovaMessage } from './sensenovaHelpers';
3
+
4
+ describe('convertSenseNovaMessage', () => {
5
+ it('should convert string content to text type array', () => {
6
+ const content = 'Hello world';
7
+ const result = convertSenseNovaMessage(content);
8
+
9
+ expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
10
+ });
11
+
12
+ it('should handle array content with text type', () => {
13
+ const content = [
14
+ { type: 'text', text: 'Hello world' }
15
+ ];
16
+ const result = convertSenseNovaMessage(content);
17
+
18
+ expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
19
+ });
20
+
21
+ it('should convert image_url with base64 format to image_base64', () => {
22
+ const content = [
23
+ { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } }
24
+ ];
25
+ const result = convertSenseNovaMessage(content);
26
+
27
+ expect(result).toEqual([
28
+ { type: 'image_base64', image_base64: 'ABCDEF123456' }
29
+ ]);
30
+ });
31
+
32
+ it('should keep image_url format for non-base64 urls', () => {
33
+ const content = [
34
+ { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
35
+ ];
36
+ const result = convertSenseNovaMessage(content);
37
+
38
+ expect(result).toEqual([
39
+ { type: 'image_url', image_url: 'https://example.com/image.jpg' }
40
+ ]);
41
+ });
42
+
43
+ it('should handle mixed content types', () => {
44
+ const content = [
45
+ { type: 'text', text: 'Hello world' },
46
+ { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } },
47
+ { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
48
+ ];
49
+ const result = convertSenseNovaMessage(content);
50
+
51
+ expect(result).toEqual([
52
+ { type: 'text', text: 'Hello world' },
53
+ { type: 'image_base64', image_base64: 'ABCDEF123456' },
54
+ { type: 'image_url', image_url: 'https://example.com/image.jpg' }
55
+ ]);
56
+ });
57
+
58
+ it('should filter out invalid items', () => {
59
+ const content = [
60
+ { type: 'text', text: 'Hello world' },
61
+ { type: 'unknown', value: 'should be filtered' },
62
+ { type: 'image_url', image_url: { notUrl: 'missing url field' } }
63
+ ];
64
+ const result = convertSenseNovaMessage(content);
65
+
66
+ expect(result).toEqual([
67
+ { type: 'text', text: 'Hello world' }
68
+ ]);
69
+ });
70
+
71
+ it('should handle the example input format correctly', () => {
72
+ const messages = [
73
+ {
74
+ content: [
75
+ {
76
+ content: "Hi",
77
+ role: "user"
78
+ },
79
+ {
80
+ image_url: {
81
+ detail: "auto",
82
+ url: "data:image/jpeg;base64,ABCDEF123456"
83
+ },
84
+ type: "image_url"
85
+ }
86
+ ],
87
+ role: "user"
88
+ }
89
+ ];
90
+
91
+ // This is simulating how you might use convertSenseNovaMessage with the example input
92
+ // Note: The actual function only converts the content part, not the entire messages array
93
+ const content = messages[0].content;
94
+
95
+ // This is how the function would be expected to handle a mixed array like this
96
+ // However, the actual test would need to be adjusted based on how your function
97
+ // is intended to handle this specific format with nested content objects
98
+ const result = convertSenseNovaMessage([
99
+ { type: 'text', text: "Hi" },
100
+ { type: 'image_url', image_url: { url: "data:image/jpeg;base64,ABCDEF123456" } }
101
+ ]);
102
+
103
+ expect(result).toEqual([
104
+ { type: 'text', text: "Hi" },
105
+ { type: 'image_base64', image_base64: "ABCDEF123456" }
106
+ ]);
107
+ });
108
+ });
@@ -0,0 +1,30 @@
1
+ export const convertSenseNovaMessage = (content: any) => {
2
+
3
+ // 如果为单条 string 类 content,则格式转换为 text 类
4
+ if (typeof content === 'string') {
5
+ return [{ text: content, type: 'text' }];
6
+ }
7
+
8
+ // 如果内容包含图片内容,则需要对 array 类 content,进行格式转换
9
+ return content
10
+ ?.map((item: any) => {
11
+ // 如果为 content,则格式转换为 text 类
12
+ if (item.type === 'text') return item;
13
+
14
+ // 如果为 image_url,则格式转换为 image_url 类
15
+ if (item.type === 'image_url' && item.image_url?.url) {
16
+ const url = item.image_url.url;
17
+
18
+ // 如果 image_url 为 base64 格式,则返回 image_base64 类,否则返回 image_url 类
19
+ return url.startsWith('data:image/jpeg;base64')
20
+ ? {
21
+ image_base64: url.split(',')[1],
22
+ type: 'image_base64',
23
+ }
24
+ : { image_url: url, type: 'image_url' };
25
+ }
26
+
27
+ return null;
28
+ })
29
+ .filter(Boolean);
30
+ };
@@ -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
  },