@lobehub/chat 1.100.2 → 1.101.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 (55) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/changelog/v1.json +12 -0
  5. package/docs/development/basic/add-new-ai-image-model.mdx +36 -0
  6. package/docs/development/basic/add-new-ai-image-model.zh-CN.mdx +0 -0
  7. package/locales/ar/components.json +5 -4
  8. package/locales/ar/models.json +3 -0
  9. package/locales/bg-BG/components.json +5 -4
  10. package/locales/bg-BG/models.json +3 -0
  11. package/locales/de-DE/components.json +5 -4
  12. package/locales/de-DE/models.json +3 -0
  13. package/locales/en-US/components.json +5 -4
  14. package/locales/en-US/models.json +3 -0
  15. package/locales/es-ES/components.json +5 -4
  16. package/locales/es-ES/models.json +3 -0
  17. package/locales/fa-IR/components.json +5 -4
  18. package/locales/fa-IR/models.json +3 -0
  19. package/locales/fr-FR/components.json +5 -4
  20. package/locales/fr-FR/models.json +3 -0
  21. package/locales/it-IT/components.json +5 -4
  22. package/locales/it-IT/models.json +3 -0
  23. package/locales/ja-JP/components.json +5 -4
  24. package/locales/ja-JP/models.json +3 -0
  25. package/locales/ko-KR/components.json +5 -4
  26. package/locales/ko-KR/models.json +3 -0
  27. package/locales/nl-NL/components.json +5 -4
  28. package/locales/nl-NL/models.json +3 -0
  29. package/locales/pl-PL/components.json +5 -4
  30. package/locales/pl-PL/models.json +3 -0
  31. package/locales/pt-BR/components.json +5 -4
  32. package/locales/pt-BR/models.json +3 -0
  33. package/locales/ru-RU/components.json +5 -4
  34. package/locales/ru-RU/models.json +3 -0
  35. package/locales/tr-TR/components.json +5 -4
  36. package/locales/tr-TR/models.json +3 -0
  37. package/locales/vi-VN/components.json +5 -4
  38. package/locales/vi-VN/models.json +3 -0
  39. package/locales/zh-CN/components.json +5 -4
  40. package/locales/zh-CN/models.json +3 -0
  41. package/locales/zh-TW/components.json +5 -4
  42. package/locales/zh-TW/models.json +3 -0
  43. package/package.json +1 -1
  44. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +24 -9
  45. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +0 -1
  46. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +161 -39
  47. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +10 -10
  48. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +153 -83
  49. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +2 -11
  50. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useDragAndDrop.ts +71 -0
  51. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/style.ts +17 -0
  52. package/src/config/aiModels/zhipu.ts +29 -6
  53. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +20 -3
  54. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +20 -11
  55. package/src/locales/default/components.ts +4 -3
@@ -12,6 +12,8 @@ import { useFileStore } from '@/store/file';
12
12
  import { FileUploadStatus } from '@/types/files/upload';
13
13
 
14
14
  import { CONFIG_PANEL_WIDTH } from '../../constants';
15
+ import { useDragAndDrop } from '../../hooks/useDragAndDrop';
16
+ import { useConfigPanelStyles } from '../../style';
15
17
  import ImageManageModal, { type ImageItem } from './ImageManageModal';
16
18
 
17
19
  // ======== Business Types ======== //
@@ -105,6 +107,14 @@ const useStyles = createStyles(({ css, token }) => {
105
107
  width: 100%;
106
108
  height: ${thumbnailSize}px;
107
109
  padding: 0;
110
+ border-radius: ${token.borderRadiusLG}px;
111
+
112
+ transition: all 0.2s ease;
113
+
114
+ &.drag-over {
115
+ transform: scale(1.02);
116
+ background: ${token.colorPrimaryBg};
117
+ }
108
118
  `,
109
119
 
110
120
  moreOverlay: css`
@@ -139,6 +149,12 @@ const useStyles = createStyles(({ css, token }) => {
139
149
  border-color: ${token.colorPrimary};
140
150
  background: ${token.colorFillSecondary};
141
151
  }
152
+
153
+ &.drag-over {
154
+ transform: scale(1.02);
155
+ border-color: ${token.colorPrimary};
156
+ background: ${token.colorPrimaryBg};
157
+ }
142
158
  `,
143
159
 
144
160
  placeholderIcon: css`
@@ -167,6 +183,11 @@ const useStyles = createStyles(({ css, token }) => {
167
183
  background: ${token.colorFillSecondary};
168
184
 
169
185
  transition: all 0.2s ease;
186
+
187
+ &.drag-over {
188
+ transform: scale(1.02);
189
+ background: ${token.colorPrimaryBg};
190
+ }
170
191
  `,
171
192
 
172
193
  progressPrimary: css`
@@ -197,6 +218,8 @@ const useStyles = createStyles(({ css, token }) => {
197
218
 
198
219
  background: ${token.colorBgContainer};
199
220
 
221
+ transition: all 0.2s ease;
222
+
200
223
  &:hover .upload-more-overlay {
201
224
  opacity: 1;
202
225
  }
@@ -204,6 +227,11 @@ const useStyles = createStyles(({ css, token }) => {
204
227
  &:hover .delete-icon {
205
228
  opacity: 1;
206
229
  }
230
+
231
+ &.drag-over {
232
+ transform: scale(1.02);
233
+ background: ${token.colorPrimaryBg};
234
+ }
207
235
  `,
208
236
  uploadMoreButton: css`
209
237
  cursor: pointer;
@@ -256,15 +284,21 @@ const isLocalBlobUrl = (url: string): boolean => url.startsWith('blob:');
256
284
  // ======== Sub-Components ======== //
257
285
 
258
286
  interface ImageUploadPlaceholderProps {
287
+ isDragOver?: boolean;
259
288
  onClick?: () => void;
260
289
  }
261
290
 
262
- const ImageUploadPlaceholder: FC<ImageUploadPlaceholderProps> = memo(({ onClick }) => {
291
+ const ImageUploadPlaceholder: FC<ImageUploadPlaceholderProps> = memo(({ isDragOver, onClick }) => {
263
292
  const { styles } = useStyles();
264
293
  const { t } = useTranslation('components');
265
294
 
266
295
  return (
267
- <Center className={styles.placeholder} gap={16} horizontal={false} onClick={onClick}>
296
+ <Center
297
+ className={`${styles.placeholder} ${isDragOver ? 'drag-over' : ''}`}
298
+ gap={16}
299
+ horizontal={false}
300
+ onClick={onClick}
301
+ >
268
302
  <ImageIcon className={styles.placeholderIcon} size={48} strokeWidth={1.5} />
269
303
  <div className={styles.placeholderText}>
270
304
  {t('MultiImagesUpload.placeholder.primary')}
@@ -408,104 +442,117 @@ ImageUploadProgress.displayName = 'ImageUploadProgress';
408
442
 
409
443
  interface ImageThumbnailsProps {
410
444
  images: string[];
445
+ isDragOver?: boolean;
411
446
  onClick?: () => void;
412
447
  onDelete?: (index: number) => void;
413
448
  }
414
449
 
415
- const ImageThumbnails: FC<ImageThumbnailsProps> = memo(({ images, onClick, onDelete }) => {
416
- const { styles } = useStyles();
450
+ const ImageThumbnails: FC<ImageThumbnailsProps> = memo(
451
+ ({ images, isDragOver, onClick, onDelete }) => {
452
+ const { styles } = useStyles();
453
+ const { styles: configStyles } = useConfigPanelStyles();
417
454
 
418
- // Display max 4 images, with overflow indication
419
- const displayImages = images.slice(0, 4);
420
- const remainingCount = Math.max(0, images.length - 3); // Show +x for images beyond first 3
455
+ // Display max 4 images, with overflow indication
456
+ const displayImages = images.slice(0, 4);
457
+ const remainingCount = Math.max(0, images.length - 3); // Show +x for images beyond first 3
421
458
 
422
- const handleDelete = (index: number, event: React.MouseEvent) => {
423
- event.stopPropagation(); // Prevent triggering container click
424
- onDelete?.(index);
425
- };
459
+ const handleDelete = (index: number, event: React.MouseEvent) => {
460
+ event.stopPropagation(); // Prevent triggering container click
461
+ onDelete?.(index);
462
+ };
426
463
 
427
- const renderImageItem = (imageUrl: string, index: number) => {
428
- const isLastItem = index === 3;
429
- const showOverlay = isLastItem && remainingCount > 1;
464
+ const renderImageItem = (imageUrl: string, index: number) => {
465
+ const isLastItem = index === 3;
466
+ const showOverlay = isLastItem && remainingCount > 1;
467
+
468
+ return (
469
+ <div className={styles.imageItem} key={imageUrl}>
470
+ <Image
471
+ alt={`Uploaded image ${index + 1}`}
472
+ fill
473
+ src={imageUrl}
474
+ style={{ objectFit: 'cover' }}
475
+ unoptimized
476
+ />
477
+ {!showOverlay && (
478
+ <div
479
+ className={`${styles.deleteIcon} delete-icon`}
480
+ onClick={(e) => handleDelete(index, e)}
481
+ >
482
+ <X size={12} />
483
+ </div>
484
+ )}
485
+ {showOverlay && <div className={styles.moreOverlay}>+{remainingCount}</div>}
486
+ </div>
487
+ );
488
+ };
430
489
 
431
490
  return (
432
- <div className={styles.imageItem} key={imageUrl}>
433
- <Image
434
- alt={`Uploaded image ${index + 1}`}
435
- fill
436
- src={imageUrl}
437
- style={{ objectFit: 'cover' }}
438
- unoptimized
439
- />
440
- {!showOverlay && (
441
- <div
442
- className={`${styles.deleteIcon} delete-icon`}
443
- onClick={(e) => handleDelete(index, e)}
444
- >
445
- <X size={12} />
446
- </div>
447
- )}
448
- {showOverlay && <div className={styles.moreOverlay}>+{remainingCount}</div>}
491
+ <div
492
+ className={`${styles.imageThumbnails} ${configStyles.dragTransition} ${isDragOver ? configStyles.dragOver : ''}`}
493
+ onClick={onClick}
494
+ >
495
+ {displayImages.map(renderImageItem)}
449
496
  </div>
450
497
  );
451
- };
452
-
453
- return (
454
- <div className={styles.imageThumbnails} onClick={onClick}>
455
- {displayImages.map(renderImageItem)}
456
- </div>
457
- );
458
- });
498
+ },
499
+ );
459
500
 
460
501
  ImageThumbnails.displayName = 'ImageThumbnails';
461
502
 
462
503
  interface SingleImageDisplayProps {
463
504
  imageUrl: string;
505
+ isDragOver?: boolean;
464
506
  onClick?: () => void;
465
507
  onDelete?: () => void;
466
508
  }
467
509
 
468
- const SingleImageDisplay: FC<SingleImageDisplayProps> = memo(({ imageUrl, onClick, onDelete }) => {
469
- const { styles } = useStyles();
470
- const { t } = useTranslation('components');
471
-
472
- const handleDelete = (event: React.MouseEvent) => {
473
- event.stopPropagation(); // Prevent triggering container click
474
- onDelete?.();
475
- };
510
+ const SingleImageDisplay: FC<SingleImageDisplayProps> = memo(
511
+ ({ imageUrl, isDragOver, onClick, onDelete }) => {
512
+ const { styles } = useStyles();
513
+ const { styles: configStyles } = useConfigPanelStyles();
514
+ const { t } = useTranslation('components');
476
515
 
477
- const handleOverlayClick = (event: React.MouseEvent) => {
478
- event.stopPropagation(); // Prevent triggering container click
479
- onClick?.();
480
- };
516
+ const handleDelete = (event: React.MouseEvent) => {
517
+ event.stopPropagation(); // Prevent triggering container click
518
+ onDelete?.();
519
+ };
481
520
 
482
- return (
483
- <div className={styles.singleImageDisplay}>
484
- <Image
485
- alt="Uploaded image"
486
- fill
487
- src={imageUrl}
488
- style={{ objectFit: 'contain' }}
489
- unoptimized
490
- />
491
-
492
- {/* Delete button */}
493
- <div className={`${styles.deleteIcon} delete-icon`} onClick={handleDelete}>
494
- <X size={12} />
495
- </div>
521
+ const handleOverlayClick = (event: React.MouseEvent) => {
522
+ event.stopPropagation(); // Prevent triggering container click
523
+ onClick?.();
524
+ };
496
525
 
497
- {/* Upload more overlay */}
526
+ return (
498
527
  <div
499
- className={`${styles.uploadMoreOverlay} upload-more-overlay`}
500
- onClick={handleOverlayClick}
528
+ className={`${styles.singleImageDisplay} ${configStyles.dragTransition} ${isDragOver ? configStyles.dragOver : ''}`}
501
529
  >
502
- <button className={styles.uploadMoreButton} type="button">
503
- {t('MultiImagesUpload.actions.uploadMore')}
504
- </button>
530
+ <Image
531
+ alt="Uploaded image"
532
+ fill
533
+ src={imageUrl}
534
+ style={{ objectFit: 'contain' }}
535
+ unoptimized
536
+ />
537
+
538
+ {/* Delete button */}
539
+ <div className={`${styles.deleteIcon} delete-icon`} onClick={handleDelete}>
540
+ <X size={12} />
541
+ </div>
542
+
543
+ {/* Upload more overlay */}
544
+ <div
545
+ className={`${styles.uploadMoreOverlay} upload-more-overlay`}
546
+ onClick={handleOverlayClick}
547
+ >
548
+ <button className={styles.uploadMoreButton} type="button">
549
+ {t('MultiImagesUpload.actions.uploadMore')}
550
+ </button>
551
+ </div>
505
552
  </div>
506
- </div>
507
- );
508
- });
553
+ );
554
+ },
555
+ );
509
556
 
510
557
  SingleImageDisplay.displayName = 'SingleImageDisplay';
511
558
 
@@ -517,6 +564,7 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
517
564
  const uploadWithProgress = useFileStore((s) => s.uploadWithProgress);
518
565
  const [displayItems, setDisplayItems] = useState<DisplayItem[]>([]);
519
566
  const [modalOpen, setModalOpen] = useState(false);
567
+ const { styles: configStyles } = useConfigPanelStyles();
520
568
 
521
569
  // Cleanup blob URLs to prevent memory leaks
522
570
  useEffect(() => {
@@ -656,6 +704,18 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
656
704
  await handleFilesSelected(Array.from(files));
657
705
  };
658
706
 
707
+ // ======== Drag and Drop Handlers ======== //
708
+
709
+ const handleDrop = async (files: File[]) => {
710
+ // Add all image files to existing images
711
+ await handleFilesSelected(files);
712
+ };
713
+
714
+ const { isDragOver, dragHandlers } = useDragAndDrop({
715
+ accept: 'image/*',
716
+ onDrop: handleDrop,
717
+ });
718
+
659
719
  // 处理 Modal 完成回调
660
720
  const handleModalComplete = async (imageItems: ImageItem[]) => {
661
721
  // 分离现有URL和新文件
@@ -685,7 +745,7 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
685
745
  const isSingleImage = value && value.length === 1;
686
746
 
687
747
  return (
688
- <div className={className} style={style}>
748
+ <div className={className} {...dragHandlers} style={style}>
689
749
  {/* Hidden file input */}
690
750
  <input
691
751
  accept="image/*"
@@ -702,22 +762,32 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
702
762
 
703
763
  {/* Conditional rendering based on state */}
704
764
  {isUploading ? (
705
- <ImageUploadProgress
706
- completedCount={completedFiles}
707
- currentProgress={overallProgress}
708
- showCount={totalFiles > 1}
709
- totalCount={totalFiles}
710
- />
765
+ <div
766
+ className={`${configStyles.dragTransition} ${isDragOver ? configStyles.dragOver : ''}`}
767
+ >
768
+ <ImageUploadProgress
769
+ completedCount={completedFiles}
770
+ currentProgress={overallProgress}
771
+ showCount={totalFiles > 1}
772
+ totalCount={totalFiles}
773
+ />
774
+ </div>
711
775
  ) : isSingleImage ? (
712
776
  <SingleImageDisplay
713
777
  imageUrl={value[0]}
778
+ isDragOver={isDragOver}
714
779
  onClick={handleOpenModal}
715
780
  onDelete={() => handleDelete(0)}
716
781
  />
717
782
  ) : hasImages ? (
718
- <ImageThumbnails images={value || []} onClick={handleOpenModal} onDelete={handleDelete} />
783
+ <ImageThumbnails
784
+ images={value || []}
785
+ isDragOver={isDragOver}
786
+ onClick={handleOpenModal}
787
+ onDelete={handleDelete}
788
+ />
719
789
  ) : (
720
- <ImageUploadPlaceholder onClick={handlePlaceholderClick} />
790
+ <ImageUploadPlaceholder isDragOver={isDragOver} onClick={handlePlaceholderClick} />
721
791
  )}
722
792
 
723
793
  {/* Image Management Modal */}
@@ -7,18 +7,9 @@ import InputNumber from '../../../components/SeedNumberInput';
7
7
 
8
8
  const SeedNumberInput = memo(() => {
9
9
  const { t } = useTranslation('image');
10
- const { value, setValue, min, max, step = 1 } = useGenerationConfigParam('seed');
10
+ const { value, setValue } = useGenerationConfigParam('seed');
11
11
 
12
- return (
13
- <InputNumber
14
- max={max}
15
- min={min}
16
- onChange={setValue as any}
17
- placeholder={t('config.seed.random')}
18
- step={step}
19
- value={value}
20
- />
21
- );
12
+ return <InputNumber onChange={setValue} placeholder={t('config.seed.random')} value={value} />;
22
13
  });
23
14
 
24
15
  export default SeedNumberInput;
@@ -0,0 +1,71 @@
1
+ import React, { useRef, useState } from 'react';
2
+
3
+ interface UseDragAndDropOptions {
4
+ accept?: string; // MIME type filter, e.g., 'image/*'
5
+ onDrop: (files: File[]) => void;
6
+ }
7
+
8
+ export const useDragAndDrop = ({ onDrop, accept = 'image/*' }: UseDragAndDropOptions) => {
9
+ const [isDragOver, setIsDragOver] = useState(false);
10
+ const dragCounter = useRef(0);
11
+
12
+ const handleDragEnter = (e: React.DragEvent) => {
13
+ e.preventDefault();
14
+ e.stopPropagation();
15
+
16
+ // Check if dragging files
17
+ if (e.dataTransfer.types.includes('Files')) {
18
+ dragCounter.current++;
19
+ setIsDragOver(true);
20
+ }
21
+ };
22
+
23
+ const handleDragLeave = (e: React.DragEvent) => {
24
+ e.preventDefault();
25
+ e.stopPropagation();
26
+
27
+ dragCounter.current--;
28
+ if (dragCounter.current === 0) {
29
+ setIsDragOver(false);
30
+ }
31
+ };
32
+
33
+ const handleDrop = async (e: React.DragEvent) => {
34
+ e.preventDefault();
35
+ e.stopPropagation();
36
+
37
+ dragCounter.current = 0;
38
+ setIsDragOver(false);
39
+
40
+ const files = Array.from(e.dataTransfer.files);
41
+
42
+ // Filter files based on accept type
43
+ const filteredFiles = files.filter((file) => {
44
+ if (accept === '*/*') return true;
45
+ if (accept.endsWith('/*')) {
46
+ const acceptType = accept.slice(0, -2);
47
+ return file.type.startsWith(acceptType);
48
+ }
49
+ return file.type === accept;
50
+ });
51
+
52
+ if (filteredFiles.length > 0) {
53
+ onDrop(filteredFiles);
54
+ }
55
+ };
56
+
57
+ const dragHandlers = {
58
+ onDragEnter: handleDragEnter,
59
+ onDragLeave: handleDragLeave,
60
+ onDragOver: (e: React.DragEvent) => {
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ },
64
+ onDrop: handleDrop,
65
+ };
66
+
67
+ return {
68
+ dragHandlers,
69
+ isDragOver,
70
+ };
71
+ };
@@ -0,0 +1,17 @@
1
+ import { createStyles } from 'antd-style';
2
+
3
+ export const useConfigPanelStyles = createStyles(({ css, token }) => ({
4
+ dragOver: css`
5
+ transform: scale(1.02);
6
+ border-color: ${token.colorPrimary} !important;
7
+ box-shadow: 0 0 0 2px ${token.colorPrimary}20;
8
+ transition: transform 0.2s ease;
9
+ `,
10
+
11
+ dragTransition: css`
12
+ transition:
13
+ transform 0.2s ease,
14
+ border-color 0.2s ease,
15
+ box-shadow 0.2s ease;
16
+ `,
17
+ }));
@@ -1,4 +1,4 @@
1
- import { AIChatModelCard } from '@/types/aiModel';
1
+ import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
2
2
 
3
3
  const zhipuChatModels: AIChatModelCard[] = [
4
4
  {
@@ -8,7 +8,8 @@ const zhipuChatModels: AIChatModelCard[] = [
8
8
  vision: true,
9
9
  },
10
10
  contextWindowTokens: 64_000,
11
- description: 'GLM-4.1V-Thinking 系列模型是目前已知10B级别的VLM模型中性能最强的视觉模型,融合了同级别SOTA的各项视觉语言任务,包括视频理解、图片问答、学科解题、OCR文字识别、文档和图表解读、GUI Agent、前端网页Coding、Grounding等,多项任务能力甚至超过8倍参数量的Qwen2.5-VL-72B。通过领先的强化学习技术,模型掌握了通过思维链推理的方式提升回答的准确性和丰富度,从最终效果和可解释性等维度都显著超过传统的非thinking模型。',
11
+ description:
12
+ 'GLM-4.1V-Thinking 系列模型是目前已知10B级别的VLM模型中性能最强的视觉模型,融合了同级别SOTA的各项视觉语言任务,包括视频理解、图片问答、学科解题、OCR文字识别、文档和图表解读、GUI Agent、前端网页Coding、Grounding等,多项任务能力甚至超过8倍参数量的Qwen2.5-VL-72B。通过领先的强化学习技术,模型掌握了通过思维链推理的方式提升回答的准确性和丰富度,从最终效果和可解释性等维度都显著超过传统的非thinking模型。',
12
13
  displayName: 'GLM-4.1V-Thinking-FlashX',
13
14
  id: 'glm-4.1v-thinking-flashx',
14
15
  pricing: {
@@ -28,7 +29,8 @@ const zhipuChatModels: AIChatModelCard[] = [
28
29
  vision: true,
29
30
  },
30
31
  contextWindowTokens: 64_000,
31
- description: 'GLM-4.1V-Thinking 系列模型是目前已知10B级别的VLM模型中性能最强的视觉模型,融合了同级别SOTA的各项视觉语言任务,包括视频理解、图片问答、学科解题、OCR文字识别、文档和图表解读、GUI Agent、前端网页Coding、Grounding等,多项任务能力甚至超过8倍参数量的Qwen2.5-VL-72B。通过领先的强化学习技术,模型掌握了通过思维链推理的方式提升回答的准确性和丰富度,从最终效果和可解释性等维度都显著超过传统的非thinking模型。',
32
+ description:
33
+ 'GLM-4.1V-Thinking 系列模型是目前已知10B级别的VLM模型中性能最强的视觉模型,融合了同级别SOTA的各项视觉语言任务,包括视频理解、图片问答、学科解题、OCR文字识别、文档和图表解读、GUI Agent、前端网页Coding、Grounding等,多项任务能力甚至超过8倍参数量的Qwen2.5-VL-72B。通过领先的强化学习技术,模型掌握了通过思维链推理的方式提升回答的准确性和丰富度,从最终效果和可解释性等维度都显著超过传统的非thinking模型。',
32
34
  displayName: 'GLM-4.1V-Thinking-Flash',
33
35
  enabled: true,
34
36
  id: 'glm-4.1v-thinking-flash',
@@ -103,8 +105,7 @@ const zhipuChatModels: AIChatModelCard[] = [
103
105
  search: true,
104
106
  },
105
107
  contextWindowTokens: 128_000,
106
- description:
107
- '高速低价:Flash增强版本,超快推理速度,更快并发保障。',
108
+ description: '高速低价:Flash增强版本,超快推理速度,更快并发保障。',
108
109
  displayName: 'GLM-Z1-FlashX',
109
110
  id: 'glm-z1-flashx',
110
111
  maxOutput: 32_000,
@@ -409,6 +410,28 @@ const zhipuChatModels: AIChatModelCard[] = [
409
410
  },
410
411
  ];
411
412
 
412
- export const allModels = [...zhipuChatModels];
413
+ const zhipuImageModels: AIImageModelCard[] = [
414
+ // https://bigmodel.cn/dev/api/image-model/cogview
415
+ {
416
+ description:
417
+ 'CogView-4 是智谱首个支持生成汉字的开源文生图模型,在语义理解、图像生成质量、中英文字生成能力等方面全面提升,支持任意长度的中英双语输入,能够生成在给定范围内的任意分辨率图像。',
418
+ displayName: 'CogView-4',
419
+ enabled: true,
420
+ id: 'cogview-4',
421
+ parameters: {
422
+ prompt: {
423
+ default: '',
424
+ },
425
+ size: {
426
+ default: '1024x1024',
427
+ enum: ['1024x1024', '768x1344', '864x1152', '1344x768', '1152x864', '1440x720', '720x1440'],
428
+ },
429
+ },
430
+ releasedAt: '2025-03-04',
431
+ type: 'image',
432
+ },
433
+ ];
434
+
435
+ export const allModels = [...zhipuChatModels, ...zhipuImageModels];
413
436
 
414
437
  export default allModels;
@@ -1227,9 +1227,26 @@ describe('LobeOpenAICompatibleFactory', () => {
1227
1227
  );
1228
1228
  });
1229
1229
 
1230
- it('should throw error when b64_json is missing', async () => {
1230
+ it('should handle url format response successfully', async () => {
1231
1231
  vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
1232
- data: [{ url: 'https://example.com/image.jpg' }],
1232
+ data: [{ url: 'https://example.com/generated-image.jpg' }],
1233
+ } as any);
1234
+
1235
+ const payload = {
1236
+ model: 'dall-e-3',
1237
+ params: { prompt: 'Test prompt' },
1238
+ };
1239
+
1240
+ const result = await (instance as any).createImage(payload);
1241
+
1242
+ expect(result).toEqual({
1243
+ imageUrl: 'https://example.com/generated-image.jpg',
1244
+ });
1245
+ });
1246
+
1247
+ it('should throw error when both b64_json and url are missing', async () => {
1248
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
1249
+ data: [{ some_other_field: 'value' }],
1233
1250
  } as any);
1234
1251
 
1235
1252
  const payload = {
@@ -1238,7 +1255,7 @@ describe('LobeOpenAICompatibleFactory', () => {
1238
1255
  };
1239
1256
 
1240
1257
  await expect((instance as any).createImage(payload)).rejects.toThrow(
1241
- 'Invalid image response: missing b64_json field',
1258
+ 'Invalid image response: missing both b64_json and url fields',
1242
1259
  );
1243
1260
  });
1244
1261
  });
@@ -390,21 +390,30 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
390
390
  throw new Error('Invalid image response: first data item is null or undefined');
391
391
  }
392
392
 
393
- if (!imageData.b64_json) {
394
- log('Invalid image response: missing b64_json field');
395
- throw new Error('Invalid image response: missing b64_json field');
396
- }
397
-
398
- // 确定图片的 MIME 类型,默认为 PNG
399
- const mimeType = 'image/png'; // OpenAI 图片生成默认返回 PNG 格式
393
+ let imageUrl: string;
400
394
 
401
- // base64 字符串转换为完整的 data URL
402
- const dataUrl = `data:${mimeType};base64,${imageData.b64_json}`;
395
+ // 处理 base64 格式的响应
396
+ if (imageData.b64_json) {
397
+ // 确定图片的 MIME 类型,默认为 PNG
398
+ const mimeType = 'image/png'; // OpenAI 图片生成默认返回 PNG 格式
403
399
 
404
- log('Successfully converted base64 to data URL, length: %d', dataUrl.length);
400
+ // base64 字符串转换为完整的 data URL
401
+ imageUrl = `data:${mimeType};base64,${imageData.b64_json}`;
402
+ log('Successfully converted base64 to data URL, length: %d', imageUrl.length);
403
+ }
404
+ // 处理 URL 格式的响应
405
+ else if (imageData.url) {
406
+ imageUrl = imageData.url;
407
+ log('Using direct image URL: %s', imageUrl);
408
+ }
409
+ // 如果两种格式都不存在,抛出错误
410
+ else {
411
+ log('Invalid image response: missing both b64_json and url fields');
412
+ throw new Error('Invalid image response: missing both b64_json and url fields');
413
+ }
405
414
 
406
415
  return {
407
- imageUrl: dataUrl,
416
+ imageUrl,
408
417
  };
409
418
  }
410
419
 
@@ -73,10 +73,11 @@ export default {
73
73
  ImageUpload: {
74
74
  actions: {
75
75
  changeImage: '点击更换图片',
76
+ dropMultipleFiles: '不支持多上传多个文件,只会使用第一个文件',
76
77
  },
77
78
  placeholder: {
78
79
  primary: '添加图片',
79
- secondary: '点击上传',
80
+ secondary: '点击或拖拽上传',
80
81
  },
81
82
  },
82
83
  KeyValueEditor: {
@@ -111,7 +112,7 @@ export default {
111
112
  },
112
113
  MultiImagesUpload: {
113
114
  actions: {
114
- uploadMore: '点击上传更多',
115
+ uploadMore: '点击或拖拽上传更多',
115
116
  },
116
117
  modal: {
117
118
  complete: '完成',
@@ -121,7 +122,7 @@ export default {
121
122
  upload: '上传图片',
122
123
  },
123
124
  placeholder: {
124
- primary: '点击上传图片',
125
+ primary: '点击或拖拽上传图片',
125
126
  secondary: '支持多张图片选择',
126
127
  },
127
128
  progress: {