@lobehub/chat 1.16.2 → 1.16.4
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.
Potentially problematic release.
This version of @lobehub/chat might be problematic. Click here for more details.
- package/CHANGELOG.md +50 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/docs/self-hosting/advanced/auth/next-auth/logto.zh-CN.mdx +2 -2
- package/locales/ar/common.json +1 -1
- package/locales/ar/error.json +2 -1
- package/locales/ar/file.json +2 -0
- package/locales/bg-BG/common.json +1 -1
- package/locales/bg-BG/error.json +2 -1
- package/locales/bg-BG/file.json +2 -0
- package/locales/de-DE/common.json +1 -1
- package/locales/de-DE/error.json +2 -1
- package/locales/de-DE/file.json +2 -0
- package/locales/en-US/common.json +1 -1
- package/locales/en-US/error.json +2 -1
- package/locales/en-US/file.json +2 -0
- package/locales/es-ES/common.json +1 -1
- package/locales/es-ES/error.json +2 -1
- package/locales/es-ES/file.json +2 -0
- package/locales/fr-FR/common.json +1 -1
- package/locales/fr-FR/error.json +2 -1
- package/locales/fr-FR/file.json +2 -0
- package/locales/it-IT/common.json +1 -1
- package/locales/it-IT/error.json +2 -1
- package/locales/it-IT/file.json +2 -0
- package/locales/ja-JP/common.json +1 -1
- package/locales/ja-JP/error.json +2 -1
- package/locales/ja-JP/file.json +2 -0
- package/locales/ko-KR/common.json +1 -1
- package/locales/ko-KR/error.json +2 -1
- package/locales/ko-KR/file.json +2 -0
- package/locales/nl-NL/common.json +1 -1
- package/locales/nl-NL/error.json +2 -1
- package/locales/nl-NL/file.json +2 -0
- package/locales/pl-PL/common.json +1 -1
- package/locales/pl-PL/error.json +2 -1
- package/locales/pl-PL/file.json +2 -0
- package/locales/pt-BR/common.json +1 -1
- package/locales/pt-BR/error.json +2 -1
- package/locales/pt-BR/file.json +2 -0
- package/locales/ru-RU/common.json +1 -1
- package/locales/ru-RU/error.json +2 -1
- package/locales/ru-RU/file.json +2 -0
- package/locales/tr-TR/common.json +1 -1
- package/locales/tr-TR/error.json +2 -1
- package/locales/tr-TR/file.json +2 -0
- package/locales/vi-VN/common.json +1 -1
- package/locales/vi-VN/error.json +2 -1
- package/locales/vi-VN/file.json +2 -0
- package/locales/zh-CN/common.json +1 -1
- package/locales/zh-CN/error.json +2 -1
- package/locales/zh-CN/file.json +2 -0
- package/locales/zh-TW/common.json +1 -1
- package/locales/zh-TW/error.json +2 -1
- package/locales/zh-TW/file.json +2 -0
- package/package.json +4 -4
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/FileItem/File.tsx +4 -1
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/FileItem/Image.tsx +4 -1
- package/src/app/(main)/files/loading.tsx +3 -1
- package/src/components/404/index.tsx +5 -1
- package/src/components/CircleLoading/index.tsx +3 -1
- package/src/components/FullscreenLoading/index.tsx +6 -2
- package/src/config/modelProviders/stepfun.ts +4 -0
- package/src/features/ChatInput/ActionBar/Upload/ServerMode.tsx +3 -3
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +3 -3
- package/src/libs/agent-runtime/google/index.test.ts +4 -1
- package/src/libs/agent-runtime/google/index.ts +3 -3
- package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +9 -3
- package/src/libs/agent-runtime/utils/anthropicHelpers.ts +2 -2
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +26 -2
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +4 -0
- package/src/libs/agent-runtime/utils/openaiHelpers.test.ts +146 -0
- package/src/libs/agent-runtime/utils/openaiHelpers.ts +40 -0
- package/src/libs/agent-runtime/zhipu/index.ts +5 -36
- package/src/locales/default/common.ts +1 -1
- package/src/locales/default/error.ts +2 -1
- package/src/locales/default/file.ts +2 -0
- package/src/utils/imageToBase64.test.ts +2 -1
- package/src/utils/imageToBase64.ts +16 -7
package/locales/ru-RU/error.json
CHANGED
@@ -16,7 +16,8 @@
|
|
16
16
|
"fetchErrorDetail": "Подробности ошибки",
|
17
17
|
"notFound": {
|
18
18
|
"backHome": "Вернуться на главную",
|
19
|
-
"
|
19
|
+
"check": "Пожалуйста, проверьте, правильный ли ваш URL",
|
20
|
+
"desc": "Мы не можем найти страницу, которую вы ищете",
|
20
21
|
"title": "Заблудились в неизведанных местах?"
|
21
22
|
},
|
22
23
|
"pluginSettings": {
|
package/locales/ru-RU/file.json
CHANGED
@@ -21,6 +21,7 @@
|
|
21
21
|
"embeddingStatus": "Векторизация"
|
22
22
|
}
|
23
23
|
},
|
24
|
+
"empty": "Нет загруженных файлов/папок",
|
24
25
|
"header": {
|
25
26
|
"actions": {
|
26
27
|
"newFolder": "Создать папку",
|
@@ -37,6 +38,7 @@
|
|
37
38
|
"new": "Создать базу знаний",
|
38
39
|
"title": "База знаний"
|
39
40
|
},
|
41
|
+
"networkError": "Не удалось получить базу знаний, пожалуйста, проверьте сетевое соединение и попробуйте снова",
|
40
42
|
"notSupportGuide": {
|
41
43
|
"desc": "Текущий развертываемый экземпляр находится в режиме клиентской базы данных, функции управления файлами недоступны. Пожалуйста, переключитесь на <1>режим серверной базы данных</1> или используйте <3>LobeChat Cloud</3> напрямую.",
|
42
44
|
"features": {
|
@@ -9,7 +9,7 @@
|
|
9
9
|
"title": "{{name}}'i Denemek İçin Hoş Geldiniz"
|
10
10
|
}
|
11
11
|
},
|
12
|
-
"appInitializing": "Uygulama başlatılıyor
|
12
|
+
"appInitializing": "Uygulama başlatılıyor...",
|
13
13
|
"autoGenerate": "Otomatik Oluştur",
|
14
14
|
"autoGenerateTooltip": "Auto-generate agent description based on prompts",
|
15
15
|
"autoGenerateTooltipDisabled": "Otomatik tamamlama işlevini kullanmadan önce ipucu kelimesini girin",
|
package/locales/tr-TR/error.json
CHANGED
@@ -16,7 +16,8 @@
|
|
16
16
|
"fetchErrorDetail": "Hata detayı",
|
17
17
|
"notFound": {
|
18
18
|
"backHome": "Ana Sayfaya Dön",
|
19
|
-
"
|
19
|
+
"check": "Lütfen URL'nizin doğru olduğundan emin olun",
|
20
|
+
"desc": "Aradığınız sayfa bulunamadı",
|
20
21
|
"title": "Bilinmeyen bir alana mı girdiniz?"
|
21
22
|
},
|
22
23
|
"pluginSettings": {
|
package/locales/tr-TR/file.json
CHANGED
@@ -21,6 +21,7 @@
|
|
21
21
|
"embeddingStatus": "Vektörleştirme"
|
22
22
|
}
|
23
23
|
},
|
24
|
+
"empty": "Henüz yüklenmiş dosya/klasör yok",
|
24
25
|
"header": {
|
25
26
|
"actions": {
|
26
27
|
"newFolder": "Yeni Klasör",
|
@@ -37,6 +38,7 @@
|
|
37
38
|
"new": "Yeni Bilgi Tabanı",
|
38
39
|
"title": "Bilgi Tabanı"
|
39
40
|
},
|
41
|
+
"networkError": "Bilgi bankası alınamadı, lütfen ağ bağlantınızı kontrol edip tekrar deneyin",
|
40
42
|
"notSupportGuide": {
|
41
43
|
"desc": "Mevcut dağıtım örneği istemci veritabanı modunda, dosya yönetim işlevini kullanamazsınız. Lütfen <1>sunucu veritabanı dağıtım moduna</1> geçin veya doğrudan <3>LobeChat Cloud</3> kullanın.",
|
42
44
|
"features": {
|
@@ -9,7 +9,7 @@
|
|
9
9
|
"title": "Chào mừng bạn trải nghiệm {{name}}"
|
10
10
|
}
|
11
11
|
},
|
12
|
-
"appInitializing": "
|
12
|
+
"appInitializing": "Đang khởi động ứng dụng...",
|
13
13
|
"autoGenerate": "Tự động tạo",
|
14
14
|
"autoGenerateTooltip": "Tự động hoàn thành mô tả trợ lý dựa trên từ gợi ý",
|
15
15
|
"autoGenerateTooltipDisabled": "Vui lòng nhập từ gợi ý trước khi sử dụng tính năng tự động hoàn thành",
|
package/locales/vi-VN/error.json
CHANGED
@@ -16,7 +16,8 @@
|
|
16
16
|
"fetchErrorDetail": "Chi tiết lỗi",
|
17
17
|
"notFound": {
|
18
18
|
"backHome": "Quay về Trang chủ",
|
19
|
-
"
|
19
|
+
"check": "Vui lòng kiểm tra xem URL của bạn có đúng không",
|
20
|
+
"desc": "Chúng tôi không thể tìm thấy trang bạn đang tìm kiếm",
|
20
21
|
"title": "Bước vào vùng đất chưa biết?"
|
21
22
|
},
|
22
23
|
"pluginSettings": {
|
package/locales/vi-VN/file.json
CHANGED
@@ -21,6 +21,7 @@
|
|
21
21
|
"embeddingStatus": "Trạng thái vector hóa"
|
22
22
|
}
|
23
23
|
},
|
24
|
+
"empty": "Chưa có tệp/tệp tin nào được tải lên",
|
24
25
|
"header": {
|
25
26
|
"actions": {
|
26
27
|
"newFolder": "Tạo thư mục mới",
|
@@ -37,6 +38,7 @@
|
|
37
38
|
"new": "Tạo kho tri thức mới",
|
38
39
|
"title": "Kho tri thức"
|
39
40
|
},
|
41
|
+
"networkError": "Không thể lấy kho tri thức, vui lòng kiểm tra kết nối mạng và thử lại",
|
40
42
|
"notSupportGuide": {
|
41
43
|
"desc": "Phiên bản triển khai hiện tại là chế độ cơ sở dữ liệu khách hàng, không thể sử dụng chức năng quản lý tệp. Vui lòng chuyển sang <1>chế độ triển khai cơ sở dữ liệu máy chủ</1>, hoặc sử dụng trực tiếp <3>LobeChat Cloud</3>",
|
42
44
|
"features": {
|
package/locales/zh-CN/error.json
CHANGED
package/locales/zh-CN/file.json
CHANGED
@@ -21,6 +21,7 @@
|
|
21
21
|
"embeddingStatus": "向量化"
|
22
22
|
}
|
23
23
|
},
|
24
|
+
"empty": "暂无已上传文件/文件夹",
|
24
25
|
"header": {
|
25
26
|
"actions": {
|
26
27
|
"newFolder": "新建文件夹",
|
@@ -37,6 +38,7 @@
|
|
37
38
|
"new": "新建知识库",
|
38
39
|
"title": "知识库"
|
39
40
|
},
|
41
|
+
"networkError": "获取知识库失败,请检测网络连接后重试",
|
40
42
|
"notSupportGuide": {
|
41
43
|
"desc": "当前部署实例为客户端数据库模式,无法使用文件管理功能。请切换到<1>服务端数据库部署模式</1>,或直接使用 <3>LobeChat Cloud</3>",
|
42
44
|
"features": {
|
package/locales/zh-TW/error.json
CHANGED
package/locales/zh-TW/file.json
CHANGED
@@ -21,6 +21,7 @@
|
|
21
21
|
"embeddingStatus": "向量化"
|
22
22
|
}
|
23
23
|
},
|
24
|
+
"empty": "暫無已上傳文件/文件夾",
|
24
25
|
"header": {
|
25
26
|
"actions": {
|
26
27
|
"newFolder": "新建資料夾",
|
@@ -37,6 +38,7 @@
|
|
37
38
|
"new": "新建知識庫",
|
38
39
|
"title": "知識庫"
|
39
40
|
},
|
41
|
+
"networkError": "獲取知識庫失敗,請檢查網路連接後重試",
|
40
42
|
"notSupportGuide": {
|
41
43
|
"desc": "當前部署實例為客戶端資料庫模式,無法使用檔案管理功能。請切換到<1>伺服器端資料庫部署模式</1>,或直接使用 <3>LobeChat Cloud</3>",
|
42
44
|
"features": {
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.16.
|
3
|
+
"version": "1.16.4",
|
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",
|
@@ -111,7 +111,7 @@
|
|
111
111
|
"@cfworker/json-schema": "^2.0.0",
|
112
112
|
"@clerk/localizations": "2.0.0",
|
113
113
|
"@clerk/nextjs": "^5.3.3",
|
114
|
-
"@clerk/themes": "^2.1.
|
114
|
+
"@clerk/themes": "^2.1.27",
|
115
115
|
"@cyntler/react-doc-viewer": "^1.16.6",
|
116
116
|
"@google/generative-ai": "^0.16.0",
|
117
117
|
"@icons-pack/react-simple-icons": "9.6.0",
|
@@ -119,9 +119,9 @@
|
|
119
119
|
"@langchain/community": "^0.2.31",
|
120
120
|
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
121
121
|
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
122
|
-
"@lobehub/icons": "^1.
|
122
|
+
"@lobehub/icons": "^1.33.3",
|
123
123
|
"@lobehub/tts": "^1.24.3",
|
124
|
-
"@lobehub/ui": "^1.150.
|
124
|
+
"@lobehub/ui": "^1.150.3",
|
125
125
|
"@neondatabase/serverless": "^0.9.4",
|
126
126
|
"@next/third-parties": "^14.2.6",
|
127
127
|
"@react-spring/web": "^9.7.3",
|
@@ -14,7 +14,9 @@ export default () => {
|
|
14
14
|
<div>
|
15
15
|
<Icon icon={LoaderCircle} size={'large'} spin />
|
16
16
|
</div>
|
17
|
-
<Typography.Text
|
17
|
+
<Typography.Text style={{ letterSpacing: '0.1em' }} type={'secondary'}>
|
18
|
+
{t('loading')}
|
19
|
+
</Typography.Text>
|
18
20
|
</Flexbox>
|
19
21
|
</Center>
|
20
22
|
);
|
@@ -30,7 +30,11 @@ const NotFound = memo(() => {
|
|
30
30
|
<h2 style={{ fontWeight: 'bold', marginTop: '1em', textAlign: 'center' }}>
|
31
31
|
{t('notFound.title')}
|
32
32
|
</h2>
|
33
|
-
<p style={{ marginBottom: '2em' }}>
|
33
|
+
<p style={{ lineHeight: '1.8', marginBottom: '2em' }}>
|
34
|
+
{t('notFound.desc')}
|
35
|
+
<br />
|
36
|
+
<div style={{ textAlign: 'center' }}>{t('notFound.check')}</div>
|
37
|
+
</p>
|
34
38
|
<Link href="/">
|
35
39
|
<Button type={'primary'}>{t('notFound.backHome')}</Button>
|
36
40
|
</Link>
|
@@ -14,7 +14,9 @@ export default () => {
|
|
14
14
|
<div>
|
15
15
|
<Icon icon={LoaderCircle} size={'large'} spin />
|
16
16
|
</div>
|
17
|
-
<Typography.Text
|
17
|
+
<Typography.Text style={{ letterSpacing: '0.1em' }} type={'secondary'}>
|
18
|
+
{t('loading')}
|
19
|
+
</Typography.Text>
|
18
20
|
</Flexbox>
|
19
21
|
</Center>
|
20
22
|
);
|
@@ -10,8 +10,12 @@ const FullscreenLoading = memo<{ title?: string }>(({ title }) => {
|
|
10
10
|
<Flexbox height={'100%'} style={{ userSelect: 'none' }} width={'100%'}>
|
11
11
|
<Center flex={1} gap={12} width={'100%'}>
|
12
12
|
<ProductLogo size={48} type={'combine'} />
|
13
|
-
<Center
|
14
|
-
|
13
|
+
<Center
|
14
|
+
gap={16}
|
15
|
+
horizontal
|
16
|
+
style={{ fontSize: '16px', lineHeight: '1.5', marginTop: '2%' }}
|
17
|
+
>
|
18
|
+
<Icon icon={Loader2} spin style={{ fontSize: '16px' }} />
|
15
19
|
{title}
|
16
20
|
</Center>
|
17
21
|
</Center>
|
@@ -32,7 +32,7 @@ const FileUpload = memo(() => {
|
|
32
32
|
const items: MenuProps['items'] = [
|
33
33
|
{
|
34
34
|
disabled: !canUploadImage,
|
35
|
-
icon: <Icon icon={ImageUp} />,
|
35
|
+
icon: <Icon icon={ImageUp} style={{ fontSize: '16px' }} />,
|
36
36
|
key: 'upload-image',
|
37
37
|
label: canUploadImage ? (
|
38
38
|
<Upload
|
@@ -54,7 +54,7 @@ const FileUpload = memo(() => {
|
|
54
54
|
),
|
55
55
|
},
|
56
56
|
{
|
57
|
-
icon: <Icon icon={FileUp} />,
|
57
|
+
icon: <Icon icon={FileUp} style={{ fontSize: '16px' }} />,
|
58
58
|
key: 'upload-file',
|
59
59
|
label: (
|
60
60
|
<Upload
|
@@ -73,7 +73,7 @@ const FileUpload = memo(() => {
|
|
73
73
|
),
|
74
74
|
},
|
75
75
|
{
|
76
|
-
icon: <Icon icon={FolderUp} />,
|
76
|
+
icon: <Icon icon={FolderUp} style={{ fontSize: '16px' }} />,
|
77
77
|
key: 'upload-folder',
|
78
78
|
label: (
|
79
79
|
<Upload
|
@@ -12,7 +12,7 @@ import Item from './Item';
|
|
12
12
|
import Loading from './Loading';
|
13
13
|
|
14
14
|
export const List = memo(() => {
|
15
|
-
const { t } = useTranslation('
|
15
|
+
const { t } = useTranslation('file');
|
16
16
|
|
17
17
|
const useFetchFilesAndKnowledgeBases = useAgentStore((s) => s.useFetchFilesAndKnowledgeBases);
|
18
18
|
|
@@ -27,10 +27,10 @@ export const List = memo(() => {
|
|
27
27
|
{error ? (
|
28
28
|
<>
|
29
29
|
<Icon icon={ServerCrash} size={{ fontSize: 80 }} />
|
30
|
-
{t('
|
30
|
+
{t('networkError')}
|
31
31
|
</>
|
32
32
|
) : (
|
33
|
-
<Empty description={t('
|
33
|
+
<Empty description={t('empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
34
34
|
)}
|
35
35
|
</Center>
|
36
36
|
) : (
|
@@ -309,7 +309,10 @@ describe('LobeGoogleAI', () => {
|
|
309
309
|
const mockBase64 = 'mockBase64Data';
|
310
310
|
|
311
311
|
// Mock the imageUrlToBase64 function
|
312
|
-
vi.spyOn(imageToBase64Module, 'imageUrlToBase64').mockResolvedValueOnce(
|
312
|
+
vi.spyOn(imageToBase64Module, 'imageUrlToBase64').mockResolvedValueOnce({
|
313
|
+
base64: mockBase64,
|
314
|
+
mimeType: 'image/png',
|
315
|
+
});
|
313
316
|
|
314
317
|
const result = await instance['convertContentToGooglePart']({
|
315
318
|
type: 'image_url',
|
@@ -133,12 +133,12 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
133
133
|
}
|
134
134
|
|
135
135
|
if (type === 'url') {
|
136
|
-
const
|
136
|
+
const { base64, mimeType } = await imageUrlToBase64(content.image_url.url);
|
137
137
|
|
138
138
|
return {
|
139
139
|
inlineData: {
|
140
|
-
data:
|
141
|
-
mimeType
|
140
|
+
data: base64,
|
141
|
+
mimeType,
|
142
142
|
},
|
143
143
|
};
|
144
144
|
}
|
@@ -53,7 +53,10 @@ describe('anthropicHelpers', () => {
|
|
53
53
|
base64: null,
|
54
54
|
type: 'url',
|
55
55
|
});
|
56
|
-
vi.mocked(imageUrlToBase64).mockResolvedValue(
|
56
|
+
vi.mocked(imageUrlToBase64).mockResolvedValue({
|
57
|
+
base64: 'convertedBase64String',
|
58
|
+
mimeType: 'image/jpg',
|
59
|
+
});
|
57
60
|
|
58
61
|
const content = {
|
59
62
|
type: 'image_url',
|
@@ -67,7 +70,7 @@ describe('anthropicHelpers', () => {
|
|
67
70
|
expect(result).toEqual({
|
68
71
|
source: {
|
69
72
|
data: 'convertedBase64String',
|
70
|
-
media_type: 'image/
|
73
|
+
media_type: 'image/jpg',
|
71
74
|
type: 'base64',
|
72
75
|
},
|
73
76
|
type: 'image',
|
@@ -80,7 +83,10 @@ describe('anthropicHelpers', () => {
|
|
80
83
|
base64: null,
|
81
84
|
type: 'url',
|
82
85
|
});
|
83
|
-
vi.mocked(imageUrlToBase64).mockResolvedValue(
|
86
|
+
vi.mocked(imageUrlToBase64).mockResolvedValue({
|
87
|
+
base64: 'convertedBase64String',
|
88
|
+
mimeType: 'image/png',
|
89
|
+
});
|
84
90
|
|
85
91
|
const content = {
|
86
92
|
type: 'image_url',
|
@@ -28,11 +28,11 @@ export const buildAnthropicBlock = async (
|
|
28
28
|
};
|
29
29
|
|
30
30
|
if (type === 'url') {
|
31
|
-
const base64 = await imageUrlToBase64(content.image_url.url);
|
31
|
+
const { base64, mimeType } = await imageUrlToBase64(content.image_url.url);
|
32
32
|
return {
|
33
33
|
source: {
|
34
34
|
data: base64 as string,
|
35
|
-
media_type:
|
35
|
+
media_type: mimeType as Anthropic.ImageBlockParam.Source['media_type'],
|
36
36
|
type: 'base64',
|
37
37
|
},
|
38
38
|
type: 'image',
|
@@ -8,6 +8,7 @@ import {
|
|
8
8
|
LobeOpenAICompatibleRuntime,
|
9
9
|
ModelProvider,
|
10
10
|
} from '@/libs/agent-runtime';
|
11
|
+
import { sleep } from '@/utils/sleep';
|
11
12
|
|
12
13
|
import * as debugStreamModule from '../debugStream';
|
13
14
|
import { LobeOpenAICompatibleFactory } from './index';
|
@@ -512,9 +513,18 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
512
513
|
describe('cancel request', () => {
|
513
514
|
it('should cancel ongoing request correctly', async () => {
|
514
515
|
const controller = new AbortController();
|
515
|
-
const mockCreateMethod = vi
|
516
|
+
const mockCreateMethod = vi
|
517
|
+
.spyOn(instance['client'].chat.completions, 'create')
|
518
|
+
.mockImplementation(
|
519
|
+
() =>
|
520
|
+
new Promise((_, reject) => {
|
521
|
+
setTimeout(() => {
|
522
|
+
reject(new DOMException('The user aborted a request.', 'AbortError'));
|
523
|
+
}, 100);
|
524
|
+
}) as any,
|
525
|
+
);
|
516
526
|
|
517
|
-
instance.chat(
|
527
|
+
const chatPromise = instance.chat(
|
518
528
|
{
|
519
529
|
messages: [{ content: 'Hello', role: 'user' }],
|
520
530
|
model: 'mistralai/mistral-7b-instruct:free',
|
@@ -523,8 +533,22 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
523
533
|
{ signal: controller.signal },
|
524
534
|
);
|
525
535
|
|
536
|
+
// 给一些时间让请求开始
|
537
|
+
await sleep(50);
|
538
|
+
|
526
539
|
controller.abort();
|
527
540
|
|
541
|
+
// 等待并断言 Promise 被拒绝
|
542
|
+
// 使用 try-catch 来捕获和验证错误
|
543
|
+
try {
|
544
|
+
await chatPromise;
|
545
|
+
// 如果 Promise 没有被拒绝,测试应该失败
|
546
|
+
expect.fail('Expected promise to be rejected');
|
547
|
+
} catch (error) {
|
548
|
+
expect((error as any).errorType).toBe('AgentRuntimeError');
|
549
|
+
expect((error as any).error.name).toBe('AbortError');
|
550
|
+
expect((error as any).error.message).toBe('The user aborted a request.');
|
551
|
+
}
|
528
552
|
expect(mockCreateMethod).toHaveBeenCalledWith(
|
529
553
|
expect.anything(),
|
530
554
|
expect.objectContaining({
|
@@ -20,6 +20,7 @@ import { desensitizeUrl } from '../desensitizeUrl';
|
|
20
20
|
import { handleOpenAIError } from '../handleOpenAIError';
|
21
21
|
import { StreamingResponse } from '../response';
|
22
22
|
import { OpenAIStream } from '../streams';
|
23
|
+
import { convertOpenAIMessages } from '../openaiHelpers';
|
23
24
|
|
24
25
|
// the model contains the following keywords is not a chat model, so we should filter them out
|
25
26
|
const CHAT_MODELS_BLOCK_LIST = [
|
@@ -158,9 +159,12 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
|
|
158
159
|
stream: payload.stream ?? true,
|
159
160
|
} as OpenAI.ChatCompletionCreateParamsStreaming);
|
160
161
|
|
162
|
+
const messages = await convertOpenAIMessages(postPayload.messages);
|
163
|
+
|
161
164
|
const response = await this.client.chat.completions.create(
|
162
165
|
{
|
163
166
|
...postPayload,
|
167
|
+
messages,
|
164
168
|
...(chatCompletion?.noUserId ? {} : { user: options?.user }),
|
165
169
|
},
|
166
170
|
{
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import OpenAI from 'openai';
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { imageUrlToBase64 } from '@/utils/imageToBase64';
|
5
|
+
|
6
|
+
import { convertMessageContent, convertOpenAIMessages } from './openaiHelpers';
|
7
|
+
import { parseDataUri } from './uriParser';
|
8
|
+
|
9
|
+
// 模拟依赖
|
10
|
+
vi.mock('@/utils/imageToBase64');
|
11
|
+
vi.mock('./uriParser');
|
12
|
+
|
13
|
+
describe('convertMessageContent', () => {
|
14
|
+
beforeEach(() => {
|
15
|
+
vi.resetAllMocks();
|
16
|
+
});
|
17
|
+
|
18
|
+
afterEach(() => {
|
19
|
+
vi.restoreAllMocks();
|
20
|
+
});
|
21
|
+
|
22
|
+
it('should return the same content if not image_url type', async () => {
|
23
|
+
const content = { type: 'text', text: 'Hello' } as OpenAI.ChatCompletionContentPart;
|
24
|
+
const result = await convertMessageContent(content);
|
25
|
+
expect(result).toEqual(content);
|
26
|
+
});
|
27
|
+
|
28
|
+
it('should convert image URL to base64 when necessary', async () => {
|
29
|
+
// 设置环境变量
|
30
|
+
process.env.LLM_VISION_IMAGE_USE_BASE64 = '1';
|
31
|
+
|
32
|
+
const content = {
|
33
|
+
type: 'image_url',
|
34
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
35
|
+
} as OpenAI.ChatCompletionContentPart;
|
36
|
+
|
37
|
+
vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
|
38
|
+
vi.mocked(imageUrlToBase64).mockResolvedValue({
|
39
|
+
base64: 'base64String',
|
40
|
+
mimeType: 'image/jpeg',
|
41
|
+
});
|
42
|
+
|
43
|
+
const result = await convertMessageContent(content);
|
44
|
+
|
45
|
+
expect(result).toEqual({
|
46
|
+
type: 'image_url',
|
47
|
+
image_url: { url: 'data:image/jpeg;base64,base64String' },
|
48
|
+
});
|
49
|
+
|
50
|
+
expect(parseDataUri).toHaveBeenCalledWith('https://example.com/image.jpg');
|
51
|
+
expect(imageUrlToBase64).toHaveBeenCalledWith('https://example.com/image.jpg');
|
52
|
+
});
|
53
|
+
|
54
|
+
it('should not convert image URL when not necessary', async () => {
|
55
|
+
process.env.LLM_VISION_IMAGE_USE_BASE64 = undefined;
|
56
|
+
|
57
|
+
const content = {
|
58
|
+
type: 'image_url',
|
59
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
60
|
+
} as OpenAI.ChatCompletionContentPart;
|
61
|
+
|
62
|
+
vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
|
63
|
+
|
64
|
+
const result = await convertMessageContent(content);
|
65
|
+
|
66
|
+
expect(result).toEqual(content);
|
67
|
+
expect(imageUrlToBase64).not.toHaveBeenCalled();
|
68
|
+
});
|
69
|
+
});
|
70
|
+
|
71
|
+
describe('convertOpenAIMessages', () => {
|
72
|
+
it('should convert string content messages', async () => {
|
73
|
+
const messages = [
|
74
|
+
{ role: 'user', content: 'Hello' },
|
75
|
+
{ role: 'assistant', content: 'Hi there' },
|
76
|
+
] as OpenAI.ChatCompletionMessageParam[];
|
77
|
+
|
78
|
+
const result = await convertOpenAIMessages(messages);
|
79
|
+
|
80
|
+
expect(result).toEqual(messages);
|
81
|
+
});
|
82
|
+
|
83
|
+
it('should convert array content messages', async () => {
|
84
|
+
const messages = [
|
85
|
+
{
|
86
|
+
role: 'user',
|
87
|
+
content: [
|
88
|
+
{ type: 'text', text: 'Hello' },
|
89
|
+
{ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } },
|
90
|
+
],
|
91
|
+
},
|
92
|
+
] as OpenAI.ChatCompletionMessageParam[];
|
93
|
+
|
94
|
+
vi.spyOn(Promise, 'all');
|
95
|
+
vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
|
96
|
+
vi.mocked(imageUrlToBase64).mockResolvedValue({
|
97
|
+
base64: 'base64String',
|
98
|
+
mimeType: 'image/jpeg',
|
99
|
+
});
|
100
|
+
|
101
|
+
process.env.LLM_VISION_IMAGE_USE_BASE64 = '1';
|
102
|
+
|
103
|
+
const result = await convertOpenAIMessages(messages);
|
104
|
+
|
105
|
+
expect(result).toEqual([
|
106
|
+
{
|
107
|
+
role: 'user',
|
108
|
+
content: [
|
109
|
+
{ type: 'text', text: 'Hello' },
|
110
|
+
{
|
111
|
+
type: 'image_url',
|
112
|
+
image_url: { url: 'data:image/jpeg;base64,base64String' },
|
113
|
+
},
|
114
|
+
],
|
115
|
+
},
|
116
|
+
]);
|
117
|
+
|
118
|
+
expect(Promise.all).toHaveBeenCalledTimes(2); // 一次用于消息数组,一次用于内容数组
|
119
|
+
|
120
|
+
process.env.LLM_VISION_IMAGE_USE_BASE64 = undefined;
|
121
|
+
});
|
122
|
+
it('should convert array content messages', async () => {
|
123
|
+
const messages = [
|
124
|
+
{
|
125
|
+
role: 'user',
|
126
|
+
content: [
|
127
|
+
{ type: 'text', text: 'Hello' },
|
128
|
+
{ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } },
|
129
|
+
],
|
130
|
+
},
|
131
|
+
] as OpenAI.ChatCompletionMessageParam[];
|
132
|
+
|
133
|
+
vi.spyOn(Promise, 'all');
|
134
|
+
vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
|
135
|
+
vi.mocked(imageUrlToBase64).mockResolvedValue({
|
136
|
+
base64: 'base64String',
|
137
|
+
mimeType: 'image/jpeg',
|
138
|
+
});
|
139
|
+
|
140
|
+
const result = await convertOpenAIMessages(messages);
|
141
|
+
|
142
|
+
expect(result).toEqual(messages);
|
143
|
+
|
144
|
+
expect(Promise.all).toHaveBeenCalledTimes(2); // 一次用于消息数组,一次用于内容数组
|
145
|
+
});
|
146
|
+
});
|