@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.
- package/.github/ISSUE_TEMPLATE/1_bug_report.yml +1 -0
- package/.github/ISSUE_TEMPLATE/2_feature_request.yml +1 -0
- package/.github/ISSUE_TEMPLATE/2_feature_request_cn.yml +1 -0
- package/.github/workflows/sync-database-schema.yml +25 -0
- package/CHANGELOG.md +42 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/changelog/v1.json +14 -0
- package/docs/developer/database-schema.dbml +569 -0
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/models.json +3 -0
- package/package.json +6 -2
- package/scripts/dbmlWorkflow/index.ts +11 -0
- package/src/config/aiModels/google.ts +17 -0
- package/src/database/client/migrations.json +10 -0
- package/src/database/migrations/0016_add_message_index.sql +3 -0
- package/src/database/migrations/meta/0016_snapshot.json +4018 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/schemas/message.ts +3 -0
- package/src/database/server/models/message.ts +20 -9
- package/src/database/server/models/user.test.ts +58 -0
- package/src/features/AlertBanner/CloudBanner.tsx +1 -1
- package/src/features/Conversation/Messages/Assistant/index.tsx +4 -1
- package/src/features/Conversation/Messages/User/index.tsx +4 -4
- package/src/libs/agent-runtime/google/index.ts +8 -2
- package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +99 -0
- package/src/libs/agent-runtime/utils/streams/google-ai.ts +69 -23
- package/src/libs/agent-runtime/utils/streams/protocol.ts +2 -0
- package/src/services/chat.ts +33 -15
- package/src/services/file/client.ts +3 -1
- package/src/services/message/server.ts +2 -2
- package/src/services/message/type.ts +2 -2
- package/src/services/upload.ts +82 -1
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +44 -4
- package/src/store/chat/slices/message/action.ts +3 -0
- package/src/store/file/slices/upload/action.ts +36 -13
- package/src/store/file/store.ts +2 -0
- package/src/tools/web-browsing/Render/PageContent/index.tsx +2 -2
- package/src/tools/web-browsing/Render/Search/SearchResult/SearchResultItem.tsx +1 -1
- package/src/types/files/upload.ts +7 -0
- package/src/types/message/base.ts +22 -1
- package/src/types/message/chat.ts +1 -6
- package/src/types/message/image.ts +11 -0
- package/src/types/message/index.ts +1 -0
- 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
|
});
|
package/src/store/file/store.ts
CHANGED
@@ -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'}
|
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} />
|
@@ -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;
|
@@ -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,
|