@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.

Files changed (79) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/docs/self-hosting/advanced/auth/next-auth/logto.zh-CN.mdx +2 -2
  5. package/locales/ar/common.json +1 -1
  6. package/locales/ar/error.json +2 -1
  7. package/locales/ar/file.json +2 -0
  8. package/locales/bg-BG/common.json +1 -1
  9. package/locales/bg-BG/error.json +2 -1
  10. package/locales/bg-BG/file.json +2 -0
  11. package/locales/de-DE/common.json +1 -1
  12. package/locales/de-DE/error.json +2 -1
  13. package/locales/de-DE/file.json +2 -0
  14. package/locales/en-US/common.json +1 -1
  15. package/locales/en-US/error.json +2 -1
  16. package/locales/en-US/file.json +2 -0
  17. package/locales/es-ES/common.json +1 -1
  18. package/locales/es-ES/error.json +2 -1
  19. package/locales/es-ES/file.json +2 -0
  20. package/locales/fr-FR/common.json +1 -1
  21. package/locales/fr-FR/error.json +2 -1
  22. package/locales/fr-FR/file.json +2 -0
  23. package/locales/it-IT/common.json +1 -1
  24. package/locales/it-IT/error.json +2 -1
  25. package/locales/it-IT/file.json +2 -0
  26. package/locales/ja-JP/common.json +1 -1
  27. package/locales/ja-JP/error.json +2 -1
  28. package/locales/ja-JP/file.json +2 -0
  29. package/locales/ko-KR/common.json +1 -1
  30. package/locales/ko-KR/error.json +2 -1
  31. package/locales/ko-KR/file.json +2 -0
  32. package/locales/nl-NL/common.json +1 -1
  33. package/locales/nl-NL/error.json +2 -1
  34. package/locales/nl-NL/file.json +2 -0
  35. package/locales/pl-PL/common.json +1 -1
  36. package/locales/pl-PL/error.json +2 -1
  37. package/locales/pl-PL/file.json +2 -0
  38. package/locales/pt-BR/common.json +1 -1
  39. package/locales/pt-BR/error.json +2 -1
  40. package/locales/pt-BR/file.json +2 -0
  41. package/locales/ru-RU/common.json +1 -1
  42. package/locales/ru-RU/error.json +2 -1
  43. package/locales/ru-RU/file.json +2 -0
  44. package/locales/tr-TR/common.json +1 -1
  45. package/locales/tr-TR/error.json +2 -1
  46. package/locales/tr-TR/file.json +2 -0
  47. package/locales/vi-VN/common.json +1 -1
  48. package/locales/vi-VN/error.json +2 -1
  49. package/locales/vi-VN/file.json +2 -0
  50. package/locales/zh-CN/common.json +1 -1
  51. package/locales/zh-CN/error.json +2 -1
  52. package/locales/zh-CN/file.json +2 -0
  53. package/locales/zh-TW/common.json +1 -1
  54. package/locales/zh-TW/error.json +2 -1
  55. package/locales/zh-TW/file.json +2 -0
  56. package/package.json +4 -4
  57. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/FileItem/File.tsx +4 -1
  58. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/FileItem/Image.tsx +4 -1
  59. package/src/app/(main)/files/loading.tsx +3 -1
  60. package/src/components/404/index.tsx +5 -1
  61. package/src/components/CircleLoading/index.tsx +3 -1
  62. package/src/components/FullscreenLoading/index.tsx +6 -2
  63. package/src/config/modelProviders/stepfun.ts +4 -0
  64. package/src/features/ChatInput/ActionBar/Upload/ServerMode.tsx +3 -3
  65. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +3 -3
  66. package/src/libs/agent-runtime/google/index.test.ts +4 -1
  67. package/src/libs/agent-runtime/google/index.ts +3 -3
  68. package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +9 -3
  69. package/src/libs/agent-runtime/utils/anthropicHelpers.ts +2 -2
  70. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +26 -2
  71. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +4 -0
  72. package/src/libs/agent-runtime/utils/openaiHelpers.test.ts +146 -0
  73. package/src/libs/agent-runtime/utils/openaiHelpers.ts +40 -0
  74. package/src/libs/agent-runtime/zhipu/index.ts +5 -36
  75. package/src/locales/default/common.ts +1 -1
  76. package/src/locales/default/error.ts +2 -1
  77. package/src/locales/default/file.ts +2 -0
  78. package/src/utils/imageToBase64.test.ts +2 -1
  79. package/src/utils/imageToBase64.ts +16 -7
@@ -16,7 +16,8 @@
16
16
  "fetchErrorDetail": "Подробности ошибки",
17
17
  "notFound": {
18
18
  "backHome": "Вернуться на главную",
19
- "desc": "Мы не можем найти страницу, которую вы ищете. Пожалуйста, проверьте правильность ссылки",
19
+ "check": "Пожалуйста, проверьте, правильный ли ваш URL",
20
+ "desc": "Мы не можем найти страницу, которую вы ищете",
20
21
  "title": "Заблудились в неизведанных местах?"
21
22
  },
22
23
  "pluginSettings": {
@@ -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, lütfen bekleyin...",
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",
@@ -16,7 +16,8 @@
16
16
  "fetchErrorDetail": "Hata detayı",
17
17
  "notFound": {
18
18
  "backHome": "Ana Sayfaya Dön",
19
- "desc": "Aradığınız sayfayı bulamadık, lütfen bağlantının doğru olduğundan emin olun",
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": {
@@ -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": "Ứng dụng đang khởi động, vui lòng chờ...",
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",
@@ -16,7 +16,8 @@
16
16
  "fetchErrorDetail": "Chi tiết lỗi",
17
17
  "notFound": {
18
18
  "backHome": "Quay về Trang chủ",
19
- "desc": "Chúng tôi không thể tìm thấy trang bạn đang tìm, vui lòng kiểm tra xem liên kết có đúng không",
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": {
@@ -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": {
@@ -9,7 +9,7 @@
9
9
  "title": "{{name}} 开始公测"
10
10
  }
11
11
  },
12
- "appInitializing": "应用启动中,请耐心等待...",
12
+ "appInitializing": "应用启动中...",
13
13
  "autoGenerate": "自动补全",
14
14
  "autoGenerateTooltip": "基于提示词自动补全助手描述",
15
15
  "autoGenerateTooltipDisabled": "请填写提示词后使用自动补全功能",
@@ -16,7 +16,8 @@
16
16
  "fetchErrorDetail": "错误详情",
17
17
  "notFound": {
18
18
  "backHome": "返回首页",
19
- "desc": "我们找不到你正在寻找的页面,请检查链接是否正确",
19
+ "check": "请检查你的 URL 是否正确",
20
+ "desc": "我们找不到你寻找的页面",
20
21
  "title": "进入了未知领域?"
21
22
  },
22
23
  "pluginSettings": {
@@ -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}}"
10
10
  }
11
11
  },
12
- "appInitializing": "應用程式初始化中,請耐心等候...",
12
+ "appInitializing": "應用啟動中...",
13
13
  "autoGenerate": "自動生成",
14
14
  "autoGenerateTooltip": "基於提示詞自動生成助手描述",
15
15
  "autoGenerateTooltipDisabled": "請填寫提示詞後使用自動補全功能",
@@ -16,7 +16,8 @@
16
16
  "fetchErrorDetail": "錯誤詳情",
17
17
  "notFound": {
18
18
  "backHome": "返回首頁",
19
- "desc": "我們找不到您正在尋找的頁面,請檢查連結是否正確",
19
+ "check": "請檢查你的 URL 是否正確",
20
+ "desc": "我們找不到您尋找的頁面",
20
21
  "title": "進入了未知領域?"
21
22
  },
22
23
  "pluginSettings": {
@@ -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.2",
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.21",
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.30.0",
122
+ "@lobehub/icons": "^1.33.3",
123
123
  "@lobehub/tts": "^1.24.3",
124
- "@lobehub/ui": "^1.150.2",
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",
@@ -64,7 +64,10 @@ const FileItem = memo<FileItemProps>(({ id, onRemove, file, status, uploadState,
64
64
  e.stopPropagation();
65
65
  onRemove?.();
66
66
  }}
67
- size={'small'}
67
+ style={{
68
+ blockSize: '32px',
69
+ fontSize: '20px',
70
+ }}
68
71
  />
69
72
  </Flexbox>
70
73
  );
@@ -56,7 +56,10 @@ const FileItem = memo<FileItemProps>(({ alt, onRemove, src, loading }) => {
56
56
  e.stopPropagation();
57
57
  onRemove?.();
58
58
  }}
59
- size={'small'}
59
+ style={{
60
+ blockSize: '28px',
61
+ fontSize: '20px',
62
+ }}
60
63
  />
61
64
  }
62
65
  alt={alt || ''}
@@ -14,7 +14,9 @@ export default () => {
14
14
  <div>
15
15
  <Icon icon={LoaderCircle} size={'large'} spin />
16
16
  </div>
17
- <Typography.Text type={'secondary'}>{t('loading')}</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' }}>{t('notFound.desc')}</p>
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 type={'secondary'}>{t('loading')}</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 gap={16} horizontal>
14
- <Icon icon={Loader2} spin />
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>
@@ -61,6 +61,10 @@ const Stepfun: ModelProviderCard = {
61
61
  id: 'stepfun',
62
62
  modelList: { showModelFetcher: true },
63
63
  name: '阶跃星辰',
64
+ smoothing: {
65
+ speed: 2,
66
+ text: true,
67
+ },
64
68
  };
65
69
 
66
70
  export default Stepfun;
@@ -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('plugin');
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('store.networkError')}
30
+ {t('networkError')}
31
31
  </>
32
32
  ) : (
33
- <Empty description={t('store.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
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(mockBase64);
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 base64Image = await imageUrlToBase64(content.image_url.url);
136
+ const { base64, mimeType } = await imageUrlToBase64(content.image_url.url);
137
137
 
138
138
  return {
139
139
  inlineData: {
140
- data: base64Image,
141
- mimeType: mimeType || 'image/png',
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('convertedBase64String');
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/png',
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('convertedBase64String');
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: (mimeType as Anthropic.ImageBlockParam.Source['media_type']) || 'image/png',
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.spyOn(instance['client'].chat.completions, 'create');
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
+ });