@lobehub/chat 1.70.10 → 1.71.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 (61) hide show
  1. package/.github/ISSUE_TEMPLATE/1_bug_report.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/2_feature_request.yml +1 -0
  3. package/.github/ISSUE_TEMPLATE/2_feature_request_cn.yml +1 -0
  4. package/.github/workflows/sync-database-schema.yml +25 -0
  5. package/CHANGELOG.md +42 -0
  6. package/README.md +1 -1
  7. package/README.zh-CN.md +1 -1
  8. package/changelog/v1.json +14 -0
  9. package/docs/developer/database-schema.dbml +569 -0
  10. package/locales/ar/models.json +3 -0
  11. package/locales/bg-BG/models.json +3 -0
  12. package/locales/de-DE/models.json +3 -0
  13. package/locales/en-US/models.json +3 -0
  14. package/locales/es-ES/models.json +3 -0
  15. package/locales/fa-IR/models.json +3 -0
  16. package/locales/fr-FR/models.json +3 -0
  17. package/locales/it-IT/models.json +3 -0
  18. package/locales/ja-JP/models.json +3 -0
  19. package/locales/ko-KR/models.json +3 -0
  20. package/locales/nl-NL/models.json +3 -0
  21. package/locales/pl-PL/models.json +3 -0
  22. package/locales/pt-BR/models.json +3 -0
  23. package/locales/ru-RU/models.json +3 -0
  24. package/locales/tr-TR/models.json +3 -0
  25. package/locales/vi-VN/models.json +3 -0
  26. package/locales/zh-CN/models.json +3 -0
  27. package/locales/zh-TW/models.json +3 -0
  28. package/package.json +6 -2
  29. package/scripts/dbmlWorkflow/index.ts +11 -0
  30. package/src/config/aiModels/google.ts +17 -0
  31. package/src/database/client/migrations.json +10 -0
  32. package/src/database/migrations/0016_add_message_index.sql +3 -0
  33. package/src/database/migrations/meta/0016_snapshot.json +4018 -0
  34. package/src/database/migrations/meta/_journal.json +7 -0
  35. package/src/database/schemas/message.ts +3 -0
  36. package/src/database/server/models/message.ts +20 -9
  37. package/src/database/server/models/user.test.ts +58 -0
  38. package/src/features/AlertBanner/CloudBanner.tsx +1 -1
  39. package/src/features/Conversation/Messages/Assistant/index.tsx +4 -1
  40. package/src/features/Conversation/Messages/User/index.tsx +4 -4
  41. package/src/libs/agent-runtime/google/index.ts +8 -2
  42. package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +99 -0
  43. package/src/libs/agent-runtime/utils/streams/google-ai.ts +69 -23
  44. package/src/libs/agent-runtime/utils/streams/protocol.ts +2 -0
  45. package/src/services/chat.ts +33 -15
  46. package/src/services/file/client.ts +3 -1
  47. package/src/services/message/server.ts +2 -2
  48. package/src/services/message/type.ts +2 -2
  49. package/src/services/upload.ts +82 -1
  50. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +44 -4
  51. package/src/store/chat/slices/message/action.ts +3 -0
  52. package/src/store/file/slices/upload/action.ts +36 -13
  53. package/src/store/file/store.ts +2 -0
  54. package/src/tools/web-browsing/Render/PageContent/index.tsx +2 -2
  55. package/src/tools/web-browsing/Render/Search/SearchResult/SearchResultItem.tsx +1 -1
  56. package/src/types/files/upload.ts +7 -0
  57. package/src/types/message/base.ts +22 -1
  58. package/src/types/message/chat.ts +1 -6
  59. package/src/types/message/image.ts +11 -0
  60. package/src/types/message/index.ts +1 -0
  61. package/src/utils/fetch/fetchSSE.ts +24 -1
@@ -18,9 +18,11 @@ import { getAiInfraStoreState } from '@/store/aiInfra/store';
18
18
  import { chatHelpers } from '@/store/chat/helpers';
19
19
  import { ChatStore } from '@/store/chat/store';
20
20
  import { messageMapKey } from '@/store/chat/utils/messageMapKey';
21
+ import { getFileStoreState } from '@/store/file/store';
21
22
  import { useSessionStore } from '@/store/session';
22
23
  import { WebBrowsingManifest } from '@/tools/web-browsing';
23
24
  import { ChatMessage, CreateMessageParams, SendMessageParams } from '@/types/message';
25
+ import { ChatImageItem } from '@/types/message/image';
24
26
  import { MessageSemanticSearchChunk } from '@/types/rag';
25
27
  import { setNamespace } from '@/utils/storeDebug';
26
28
 
@@ -533,6 +535,8 @@ export const generateAIChat: StateCreator<
533
535
  let thinking = '';
534
536
  let thinkingStartAt: number;
535
537
  let duration: number;
538
+ // to upload image
539
+ const uploadTasks: Map<string, Promise<{ id?: string; url?: string }>> = new Map();
536
540
 
537
541
  const historySummary = topicSelectors.currentActiveTopicSummary(get());
538
542
  await chatService.createAssistantMessageStream({
@@ -569,6 +573,21 @@ export const generateAIChat: StateCreator<
569
573
  });
570
574
  }
571
575
 
576
+ // 等待所有图片上传完成
577
+ let finalImages: ChatImageItem[] = [];
578
+
579
+ if (uploadTasks.size > 0) {
580
+ try {
581
+ // 等待所有上传任务完成
582
+ const uploadResults = await Promise.all(uploadTasks.values());
583
+
584
+ // 使用上传后的 S3 URL 替换原始图像数据
585
+ finalImages = uploadResults.filter((i) => !!i.url) as ChatImageItem[];
586
+ } catch (error) {
587
+ console.error('Error waiting for image uploads:', error);
588
+ }
589
+ }
590
+
572
591
  if (toolCalls && toolCalls.length > 0) {
573
592
  internal_toggleToolCallingStreaming(messageId, undefined);
574
593
  }
@@ -579,6 +598,7 @@ export const generateAIChat: StateCreator<
579
598
  reasoning: !!reasoning ? { ...reasoning, duration } : undefined,
580
599
  search: !!grounding?.citations ? grounding : undefined,
581
600
  metadata: usage,
601
+ imageList: finalImages.length > 0 ? finalImages : undefined,
582
602
  });
583
603
  },
584
604
  onMessageHandle: async (chunk) => {
@@ -605,6 +625,29 @@ export const generateAIChat: StateCreator<
605
625
  break;
606
626
  }
607
627
 
628
+ case 'base64_image': {
629
+ internal_dispatchMessage({
630
+ id: messageId,
631
+ type: 'updateMessage',
632
+ value: {
633
+ imageList: chunk.images.map((i) => ({ id: i.id, url: i.data, alt: i.id })),
634
+ },
635
+ });
636
+ const image = chunk.image;
637
+
638
+ const task = getFileStoreState()
639
+ .uploadBase64FileWithProgress(image.data)
640
+ .then((value) => ({
641
+ id: value?.id,
642
+ url: value?.url,
643
+ alt: value?.filename || value?.id,
644
+ }));
645
+
646
+ uploadTasks.set(image.id, task);
647
+
648
+ break;
649
+ }
650
+
608
651
  case 'text': {
609
652
  output += chunk.text;
610
653
 
@@ -658,10 +701,7 @@ export const generateAIChat: StateCreator<
658
701
 
659
702
  internal_toggleChatLoading(false, messageId, n('generateMessage(end)') as string);
660
703
 
661
- return {
662
- isFunctionCall,
663
- traceId: msgTraceId,
664
- };
704
+ return { isFunctionCall, traceId: msgTraceId };
665
705
  },
666
706
 
667
707
  internal_resendMessage: async (
@@ -21,6 +21,7 @@ import {
21
21
  MessageToolCall,
22
22
  ModelReasoning,
23
23
  } from '@/types/message';
24
+ import { ChatImageItem } from '@/types/message/image';
24
25
  import { GroundingSearch } from '@/types/search';
25
26
  import { TraceEventPayloads } from '@/types/trace';
26
27
  import { setNamespace } from '@/utils/storeDebug';
@@ -81,6 +82,7 @@ export interface ChatMessageAction {
81
82
  reasoning?: ModelReasoning;
82
83
  search?: GroundingSearch;
83
84
  metadata?: MessageMetadata;
85
+ imageList?: ChatImageItem[];
84
86
  model?: string;
85
87
  provider?: string;
86
88
  },
@@ -319,6 +321,7 @@ export const chatMessage: StateCreator<
319
321
  metadata: extra?.metadata,
320
322
  model: extra?.model,
321
323
  provider: extra?.provider,
324
+ imageList: extra?.imageList,
322
325
  });
323
326
  await refreshMessages();
324
327
  },
@@ -11,21 +11,23 @@ import { FileMetadata, UploadFileItem } from '@/types/files';
11
11
 
12
12
  import { FileStore } from '../../store';
13
13
 
14
+ type OnStatusUpdate = (
15
+ data:
16
+ | {
17
+ id: string;
18
+ type: 'updateFile';
19
+ value: Partial<UploadFileItem>;
20
+ }
21
+ | {
22
+ id: string;
23
+ type: 'removeFile';
24
+ },
25
+ ) => void;
26
+
14
27
  interface UploadWithProgressParams {
15
28
  file: File;
16
29
  knowledgeBaseId?: string;
17
- onStatusUpdate?: (
18
- data:
19
- | {
20
- id: string;
21
- type: 'updateFile';
22
- value: Partial<UploadFileItem>;
23
- }
24
- | {
25
- id: string;
26
- type: 'removeFile';
27
- },
28
- ) => void;
30
+ onStatusUpdate?: OnStatusUpdate;
29
31
  /**
30
32
  * Optional flag to indicate whether to skip the file type check.
31
33
  * When set to `true`, any file type checks will be bypassed.
@@ -35,11 +37,19 @@ interface UploadWithProgressParams {
35
37
  }
36
38
 
37
39
  interface UploadWithProgressResult {
40
+ filename?: string;
38
41
  id: string;
39
42
  url: string;
40
43
  }
41
44
 
42
45
  export interface FileUploadAction {
46
+ uploadBase64FileWithProgress: (
47
+ base64: string,
48
+ params?: {
49
+ onStatusUpdate?: OnStatusUpdate;
50
+ },
51
+ ) => Promise<UploadWithProgressResult | undefined>;
52
+
43
53
  uploadWithProgress: (
44
54
  params: UploadWithProgressParams,
45
55
  ) => Promise<UploadWithProgressResult | undefined>;
@@ -51,6 +61,19 @@ export const createFileUploadSlice: StateCreator<
51
61
  [],
52
62
  FileUploadAction
53
63
  > = () => ({
64
+ uploadBase64FileWithProgress: async (base64) => {
65
+ const { metadata, fileType, size, hash } = await uploadService.uploadBase64ToS3(base64);
66
+
67
+ const res = await fileService.createFile({
68
+ fileType,
69
+ hash,
70
+ metadata,
71
+ name: metadata.filename,
72
+ size: size,
73
+ url: metadata.path,
74
+ });
75
+ return { ...res, filename: metadata.filename };
76
+ },
54
77
  uploadWithProgress: async ({ file, onStatusUpdate, knowledgeBaseId, skipCheckFileType }) => {
55
78
  const fileArrayBuffer = await file.arrayBuffer();
56
79
 
@@ -135,6 +158,6 @@ export const createFileUploadSlice: StateCreator<
135
158
  },
136
159
  });
137
160
 
138
- return data;
161
+ return { ...data, filename: file.name };
139
162
  },
140
163
  });
@@ -32,3 +32,5 @@ const createStore: StateCreator<FileStore, [['zustand/devtools', never]]> = (...
32
32
  const devtools = createDevtools('file');
33
33
 
34
34
  export const useFileStore = createWithEqualityFn<FileStore>()(devtools(createStore), shallow);
35
+
36
+ export const getFileStoreState = () => useFileStore.getState();
@@ -17,8 +17,8 @@ const PagesContent = memo<PagesContentProps>(({ results, messageId, urls }) => {
17
17
  if (!results || results.length === 0) {
18
18
  return (
19
19
  <Flexbox gap={12} horizontal>
20
- {urls.map((url) => (
21
- <Loading key={url} url={url} />
20
+ {urls.map((url, index) => (
21
+ <Loading key={`${url}_${index}`} url={url} />
22
22
  ))}
23
23
  </Flexbox>
24
24
  );
@@ -50,7 +50,7 @@ const SearchResultItem = memo<SearchResult>(({ url, title }) => {
50
50
  const host = urlObj.hostname;
51
51
  return (
52
52
  <Link href={url} target={'_blank'}>
53
- <Flexbox className={styles.container} gap={2} justify={'space-between'} key={url}>
53
+ <Flexbox className={styles.container} gap={2} justify={'space-between'}>
54
54
  <div className={styles.title}>{title}</div>
55
55
  <Flexbox align={'center'} gap={4} horizontal>
56
56
  <WebFavicon size={14} title={title} url={url} />
@@ -95,3 +95,10 @@ export interface CheckFileHashResult {
95
95
  size?: number;
96
96
  url?: string;
97
97
  }
98
+
99
+ export interface UploadBase64ToS3Result {
100
+ fileType: string;
101
+ hash: string;
102
+ metadata: FileMetadata;
103
+ size: number;
104
+ }
@@ -1,3 +1,6 @@
1
+ import { ChatMessageError } from '@/types/message/chat';
2
+ import { ChatImageItem } from '@/types/message/image';
3
+ import { ChatToolPayload, MessageToolCall } from '@/types/message/tools';
1
4
  import { GroundingSearch } from '@/types/search';
2
5
 
3
6
  export interface CitationItem {
@@ -22,12 +25,17 @@ export interface ModelTokensUsage {
22
25
  * currently only pplx has citation_tokens
23
26
  */
24
27
  inputCitationTokens?: number;
28
+ /**
29
+ * user prompt image
30
+ */
31
+ inputImageTokens?: number;
25
32
  /**
26
33
  * user prompt input
27
34
  */
28
35
  inputTextTokens?: number;
29
36
  inputWriteCacheTokens?: number;
30
37
  outputAudioTokens?: number;
38
+ outputImageTokens?: number;
31
39
  outputReasoningTokens?: number;
32
40
  outputTextTokens?: number;
33
41
  rejectedPredictionTokens?: number;
@@ -61,7 +69,6 @@ export interface MessageItem {
61
69
  search: GroundingSearch | null;
62
70
  sessionId: string | null;
63
71
  threadId: string | null;
64
- // jsonb type
65
72
  tools: any | null;
66
73
  topicId: string | null;
67
74
  // jsonb type
@@ -96,3 +103,17 @@ export interface NewMessage {
96
103
  updatedAt?: Date;
97
104
  userId: string; // optional because it's generated
98
105
  }
106
+
107
+ export interface UpdateMessageParams {
108
+ content?: string;
109
+ error?: ChatMessageError | null;
110
+ imageList?: ChatImageItem[];
111
+ metadata?: MessageMetadata;
112
+ model?: string;
113
+ provider?: string;
114
+ reasoning?: ModelReasoning;
115
+ role?: string;
116
+ search?: GroundingSearch;
117
+ toolCalls?: MessageToolCall[];
118
+ tools?: ChatToolPayload[] | null;
119
+ }
@@ -7,6 +7,7 @@ import { MessageSemanticSearchChunk } from '@/types/rag';
7
7
  import { GroundingSearch } from '@/types/search';
8
8
 
9
9
  import { MessageMetadata, MessageRoleType, ModelReasoning } from './base';
10
+ import { ChatImageItem } from './image';
10
11
  import { ChatPluginPayload, ChatToolPayload } from './tools';
11
12
  import { Translate } from './translate';
12
13
 
@@ -37,12 +38,6 @@ export interface ChatFileItem {
37
38
  url: string;
38
39
  }
39
40
 
40
- export interface ChatImageItem {
41
- alt: string;
42
- id: string;
43
- url: string;
44
- }
45
-
46
41
  export interface ChatFileChunk {
47
42
  fileId: string;
48
43
  fileType: string;
@@ -0,0 +1,11 @@
1
+ export interface ChatImageItem {
2
+ alt: string;
3
+ id: string;
4
+ url: string;
5
+ }
6
+
7
+ export interface ChatImageChunk {
8
+ data: string;
9
+ id: string;
10
+ isBase64?: boolean;
11
+ }
@@ -2,6 +2,7 @@ import { UploadFileItem } from '@/types/files';
2
2
 
3
3
  export * from './base';
4
4
  export * from './chat';
5
+ export * from './image';
5
6
  export * from './tools';
6
7
 
7
8
  export interface SendMessageParams {
@@ -12,7 +12,9 @@ import {
12
12
  ModelReasoning,
13
13
  ModelTokensUsage,
14
14
  } from '@/types/message';
15
+ import { ChatImageChunk } from '@/types/message/image';
15
16
  import { GroundingSearch } from '@/types/search';
17
+ import { nanoid } from '@/utils/uuid';
16
18
 
17
19
  import { fetchEventSource } from './fetchEventSource';
18
20
  import { getMessageError } from './parseError';
@@ -24,6 +26,7 @@ export type OnFinishHandler = (
24
26
  text: string,
25
27
  context: {
26
28
  grounding?: GroundingSearch;
29
+ images?: ChatImageChunk[];
27
30
  observationId?: string | null;
28
31
  reasoning?: ModelReasoning;
29
32
  toolCalls?: MessageToolCall[];
@@ -43,6 +46,13 @@ export interface MessageTextChunk {
43
46
  type: 'text';
44
47
  }
45
48
 
49
+ export interface MessageBase64ImageChunk {
50
+ id: string;
51
+ image: ChatImageChunk;
52
+ images: ChatImageChunk[];
53
+ type: 'base64_image';
54
+ }
55
+
46
56
  export interface MessageReasoningChunk {
47
57
  signature?: string;
48
58
  text?: string;
@@ -71,7 +81,8 @@ export interface FetchSSEOptions {
71
81
  | MessageToolCallsChunk
72
82
  | MessageReasoningChunk
73
83
  | MessageGroundingChunk
74
- | MessageUsageChunk,
84
+ | MessageUsageChunk
85
+ | MessageBase64ImageChunk,
75
86
  ) => void;
76
87
  smoothing?: SmoothingParams | boolean;
77
88
  }
@@ -330,6 +341,8 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
330
341
 
331
342
  let grounding: GroundingSearch | undefined = undefined;
332
343
  let usage: ModelTokensUsage | undefined = undefined;
344
+ let images: ChatImageChunk[] = [];
345
+
333
346
  await fetchEventSource(url, {
334
347
  body: options.body,
335
348
  fetch: options?.fetcher,
@@ -389,6 +402,15 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
389
402
  break;
390
403
  }
391
404
 
405
+ case 'base64_image': {
406
+ const id = 'tmp_img_' + nanoid();
407
+ const item = { data, id, isBase64: true };
408
+ images.push(item);
409
+
410
+ options.onMessageHandle?.({ id, image: item, images, type: 'base64_image' });
411
+ break;
412
+ }
413
+
392
414
  case 'text': {
393
415
  // skip empty text
394
416
  if (!data) break;
@@ -492,6 +514,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
492
514
 
493
515
  await options?.onFinish?.(output, {
494
516
  grounding,
517
+ images: images.length > 0 ? images : undefined,
495
518
  observationId,
496
519
  reasoning: !!thinking ? { content: thinking, signature: thinkingSignature } : undefined,
497
520
  toolCalls,