@lobehub/chat 1.71.4 → 1.72.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/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/docs/developer/database-schema.dbml +16 -0
- package/package.json +3 -3
- package/src/database/client/db.ts +14 -8
- package/src/database/client/migrations.json +62 -0
- package/src/database/migrations/0017_add_user_id_to_tables.sql +225 -0
- package/src/database/migrations/meta/0017_snapshot.json +3858 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/{server/models → models}/__tests__/_test_template.ts +2 -2
- package/src/database/models/__tests__/_util.ts +12 -0
- package/src/database/{server/models → models}/__tests__/agent.test.ts +6 -5
- package/src/database/{server/models → models}/__tests__/aiModel.test.ts +5 -4
- package/src/database/{server/models → models}/__tests__/aiProvider.test.ts +5 -4
- package/src/database/{server/models → models}/__tests__/asyncTask.test.ts +5 -4
- package/src/database/{server/models → models}/__tests__/chunk.test.ts +25 -21
- package/src/database/{server/models → models}/__tests__/file.test.ts +19 -5
- package/src/database/{server/models → models}/__tests__/knowledgeBase.test.ts +9 -4
- package/src/database/{server/models → models}/__tests__/message.test.ts +625 -29
- package/src/database/{server/models → models}/__tests__/plugin.test.ts +5 -4
- package/src/database/{server/models → models}/__tests__/session.test.ts +23 -20
- package/src/database/{server/models → models}/__tests__/sessionGroup.test.ts +5 -4
- package/src/database/{server/models → models}/__tests__/topic.test.ts +5 -4
- package/src/database/repositories/dataImporter/index.ts +3 -0
- package/src/database/schemas/file.ts +38 -32
- package/src/database/schemas/message.ts +21 -0
- package/src/database/schemas/relations.ts +10 -0
- package/src/database/server/models/__tests__/nextauth.test.ts +2 -0
- package/src/database/server/models/__tests__/user.test.ts +13 -1
- package/src/database/server/models/chunk.ts +5 -1
- package/src/database/server/models/file.ts +6 -3
- package/src/database/server/models/message.ts +29 -12
- package/src/database/server/models/session.ts +1 -0
- package/src/features/ShareModal/ShareImage/index.tsx +27 -11
- package/src/hooks/useImgToClipboard.ts +29 -0
- package/src/hooks/useScreenshot.ts +53 -40
- package/src/services/file/client.test.ts +2 -1
- package/src/services/message/client.test.ts +3 -3
- package/src/services/session/client.test.ts +5 -3
- package/src/types/message/base.ts +7 -0
- package/vitest.server.config.ts +1 -1
- package/src/database/server/models/user.test.ts +0 -58
- /package/src/database/{server/models → models}/__tests__/fixtures/embedding.ts +0 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
import { App } from 'antd';
|
2
|
+
import { t } from 'i18next';
|
3
|
+
import { useState } from 'react';
|
4
|
+
|
5
|
+
import { ImageType, getImageUrl } from './useScreenshot';
|
6
|
+
|
7
|
+
export const useImgToClipboard = ({ id = '#preview', width }: { id?: string; width?: number }) => {
|
8
|
+
const [loading, setLoading] = useState(false);
|
9
|
+
const { message } = App.useApp();
|
10
|
+
|
11
|
+
const handleCopy = async () => {
|
12
|
+
setLoading(true);
|
13
|
+
try {
|
14
|
+
const dataUrl = await getImageUrl({ id, imageType: ImageType.PNG, width });
|
15
|
+
const blob = await fetch(dataUrl).then((res) => res.blob());
|
16
|
+
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
17
|
+
setLoading(false);
|
18
|
+
message.success(t('copySuccess', { defaultValue: 'Copy Success', ns: 'common' }));
|
19
|
+
} catch (error) {
|
20
|
+
console.error('Failed to copy image', error);
|
21
|
+
setLoading(false);
|
22
|
+
}
|
23
|
+
};
|
24
|
+
|
25
|
+
return {
|
26
|
+
loading,
|
27
|
+
onCopy: handleCopy,
|
28
|
+
};
|
29
|
+
};
|
@@ -31,6 +31,58 @@ export const imageTypeOptions: SegmentedProps['options'] = [
|
|
31
31
|
},
|
32
32
|
];
|
33
33
|
|
34
|
+
export const getImageUrl = async ({
|
35
|
+
imageType,
|
36
|
+
id = '#preview',
|
37
|
+
width,
|
38
|
+
}: {
|
39
|
+
id?: string;
|
40
|
+
imageType: ImageType;
|
41
|
+
width?: number;
|
42
|
+
}) => {
|
43
|
+
let screenshotFn: any;
|
44
|
+
switch (imageType) {
|
45
|
+
case ImageType.JPG: {
|
46
|
+
screenshotFn = domToJpeg;
|
47
|
+
break;
|
48
|
+
}
|
49
|
+
case ImageType.PNG: {
|
50
|
+
screenshotFn = domToPng;
|
51
|
+
break;
|
52
|
+
}
|
53
|
+
case ImageType.SVG: {
|
54
|
+
screenshotFn = domToSvg;
|
55
|
+
break;
|
56
|
+
}
|
57
|
+
case ImageType.WEBP: {
|
58
|
+
screenshotFn = domToWebp;
|
59
|
+
break;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
|
64
|
+
let copy: HTMLDivElement = dom;
|
65
|
+
|
66
|
+
if (width) {
|
67
|
+
copy = dom.cloneNode(true) as HTMLDivElement;
|
68
|
+
copy.style.width = `${width}px`;
|
69
|
+
document.body.append(copy);
|
70
|
+
}
|
71
|
+
|
72
|
+
const dataUrl = await screenshotFn(width ? copy : dom, {
|
73
|
+
features: {
|
74
|
+
// 不启用移除控制符,否则会导致 safari emoji 报错
|
75
|
+
removeControlCharacter: false,
|
76
|
+
},
|
77
|
+
scale: 2,
|
78
|
+
width,
|
79
|
+
});
|
80
|
+
|
81
|
+
if (width && copy) copy?.remove();
|
82
|
+
|
83
|
+
return dataUrl;
|
84
|
+
};
|
85
|
+
|
34
86
|
export const useScreenshot = ({
|
35
87
|
imageType,
|
36
88
|
title = 'share',
|
@@ -47,46 +99,7 @@ export const useScreenshot = ({
|
|
47
99
|
const handleDownload = useCallback(async () => {
|
48
100
|
setLoading(true);
|
49
101
|
try {
|
50
|
-
|
51
|
-
switch (imageType) {
|
52
|
-
case ImageType.JPG: {
|
53
|
-
screenshotFn = domToJpeg;
|
54
|
-
break;
|
55
|
-
}
|
56
|
-
case ImageType.PNG: {
|
57
|
-
screenshotFn = domToPng;
|
58
|
-
break;
|
59
|
-
}
|
60
|
-
case ImageType.SVG: {
|
61
|
-
screenshotFn = domToSvg;
|
62
|
-
break;
|
63
|
-
}
|
64
|
-
case ImageType.WEBP: {
|
65
|
-
screenshotFn = domToWebp;
|
66
|
-
break;
|
67
|
-
}
|
68
|
-
}
|
69
|
-
|
70
|
-
const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
|
71
|
-
let copy: HTMLDivElement = dom;
|
72
|
-
|
73
|
-
if (width) {
|
74
|
-
copy = dom.cloneNode(true) as HTMLDivElement;
|
75
|
-
copy.style.width = `${width}px`;
|
76
|
-
document.body.append(copy);
|
77
|
-
}
|
78
|
-
|
79
|
-
const dataUrl = await screenshotFn(width ? copy : dom, {
|
80
|
-
features: {
|
81
|
-
// 不启用移除控制符,否则会导致 safari emoji 报错
|
82
|
-
removeControlCharacter: false,
|
83
|
-
},
|
84
|
-
scale: 2,
|
85
|
-
width,
|
86
|
-
});
|
87
|
-
|
88
|
-
if (width && copy) copy?.remove();
|
89
|
-
|
102
|
+
const dataUrl = await getImageUrl({ id, imageType, width });
|
90
103
|
const link = document.createElement('a');
|
91
104
|
link.download = `${BRANDING_NAME}_${title}_${dayjs().format('YYYY-MM-DD')}.${imageType}`;
|
92
105
|
link.href = dataUrl;
|
@@ -92,7 +92,7 @@ describe('FileService', () => {
|
|
92
92
|
hashId: '123tttt',
|
93
93
|
};
|
94
94
|
|
95
|
-
await clientDB.insert(globalFiles).values(file);
|
95
|
+
await clientDB.insert(globalFiles).values({ ...file, creator: userId });
|
96
96
|
|
97
97
|
await clientDB.insert(files).values({
|
98
98
|
id: fileId,
|
@@ -174,6 +174,7 @@ describe('FileService', () => {
|
|
174
174
|
await clientDB.insert(globalFiles).values({
|
175
175
|
...mockFile,
|
176
176
|
hashId: hash,
|
177
|
+
creator: userId,
|
177
178
|
});
|
178
179
|
await clientDB.insert(files).values({
|
179
180
|
id: '1',
|
@@ -284,7 +284,7 @@ describe('MessageClientService', () => {
|
|
284
284
|
it('should update the plugin state of a message', async () => {
|
285
285
|
// Setup
|
286
286
|
await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId });
|
287
|
-
await clientDB.insert(messagePlugins).values({ id: mockMessageId });
|
287
|
+
await clientDB.insert(messagePlugins).values({ id: mockMessageId, userId });
|
288
288
|
const key = 'stateKey';
|
289
289
|
const value = 'stateValue';
|
290
290
|
const newPluginState = { [key]: value };
|
@@ -304,7 +304,7 @@ describe('MessageClientService', () => {
|
|
304
304
|
it('should update the plugin arguments object of a message', async () => {
|
305
305
|
// Setup
|
306
306
|
await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId });
|
307
|
-
await clientDB.insert(messagePlugins).values({ id: mockMessageId });
|
307
|
+
await clientDB.insert(messagePlugins).values({ id: mockMessageId, userId });
|
308
308
|
const value = 'stateValue';
|
309
309
|
|
310
310
|
// Execute
|
@@ -319,7 +319,7 @@ describe('MessageClientService', () => {
|
|
319
319
|
it('should update the plugin arguments string of a message', async () => {
|
320
320
|
// Setup
|
321
321
|
await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId });
|
322
|
-
await clientDB.insert(messagePlugins).values({ id: mockMessageId });
|
322
|
+
await clientDB.insert(messagePlugins).values({ id: mockMessageId, userId });
|
323
323
|
const value = 'stateValue';
|
324
324
|
// Execute
|
325
325
|
await messageService.updateMessagePluginArguments(
|
@@ -32,7 +32,9 @@ beforeEach(async () => {
|
|
32
32
|
await trx.insert(users).values([{ id: userId }, { id: '456' }]);
|
33
33
|
await trx.insert(sessions).values([{ id: mockSessionId, userId }]);
|
34
34
|
await trx.insert(agents).values([{ id: mockAgentId, userId }]);
|
35
|
-
await trx
|
35
|
+
await trx
|
36
|
+
.insert(agentsToSessions)
|
37
|
+
.values([{ agentId: mockAgentId, sessionId: mockSessionId, userId }]);
|
36
38
|
await trx.insert(sessionGroups).values([
|
37
39
|
{ id: 'group-1', name: 'group-A', sort: 2, userId },
|
38
40
|
{ id: 'group-2', name: 'group-B', sort: 1, userId },
|
@@ -176,7 +178,7 @@ describe('SessionService', () => {
|
|
176
178
|
await clientDB.insert(agents).values({ userId, id: 'agent-1', title: 'Session Name' });
|
177
179
|
await clientDB
|
178
180
|
.insert(agentsToSessions)
|
179
|
-
.values({ agentId: 'agent-1', sessionId: mockSessionId });
|
181
|
+
.values({ agentId: 'agent-1', sessionId: mockSessionId, userId });
|
180
182
|
|
181
183
|
// Execute
|
182
184
|
const keyword = 'Name';
|
@@ -201,7 +203,7 @@ describe('SessionService', () => {
|
|
201
203
|
await clientDB.insert(agents).values({ userId, id: 'agent-1' });
|
202
204
|
await clientDB
|
203
205
|
.insert(agentsToSessions)
|
204
|
-
.values({ agentId: 'agent-1', sessionId: 'duplicated-session-id' });
|
206
|
+
.values({ agentId: 'agent-1', sessionId: 'duplicated-session-id', userId });
|
205
207
|
|
206
208
|
// Execute
|
207
209
|
const duplicatedSessionId = await sessionService.cloneSession(mockSessionId, newTitle);
|
@@ -117,3 +117,10 @@ export interface UpdateMessageParams {
|
|
117
117
|
toolCalls?: MessageToolCall[];
|
118
118
|
tools?: ChatToolPayload[] | null;
|
119
119
|
}
|
120
|
+
|
121
|
+
export interface NewMessageQueryParams {
|
122
|
+
embeddingsId: string;
|
123
|
+
messageId: string;
|
124
|
+
rewriteQuery: string;
|
125
|
+
userQuery: string;
|
126
|
+
}
|
package/vitest.server.config.ts
CHANGED
@@ -15,7 +15,7 @@ export default defineConfig({
|
|
15
15
|
reportsDirectory: './coverage/server',
|
16
16
|
},
|
17
17
|
environment: 'node',
|
18
|
-
include: ['src/database/server/**/**/*.test.ts'],
|
18
|
+
include: ['src/database/models/**/**/*.test.ts', 'src/database/server/**/**/*.test.ts'],
|
19
19
|
poolOptions: {
|
20
20
|
threads: { singleThread: true },
|
21
21
|
},
|
@@ -1,58 +0,0 @@
|
|
1
|
-
// @vitest-environment node
|
2
|
-
import { TRPCError } from '@trpc/server';
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
4
|
-
|
5
|
-
import { UserModel, UserNotFoundError } from '@/database/server/models/user';
|
6
|
-
|
7
|
-
describe('UserNotFoundError', () => {
|
8
|
-
it('should extend TRPCError with correct code and message', () => {
|
9
|
-
const error = new UserNotFoundError();
|
10
|
-
|
11
|
-
expect(error).toBeInstanceOf(TRPCError);
|
12
|
-
expect(error.code).toBe('UNAUTHORIZED');
|
13
|
-
expect(error.message).toBe('user not found');
|
14
|
-
});
|
15
|
-
});
|
16
|
-
|
17
|
-
describe('UserModel', () => {
|
18
|
-
const mockDb = {
|
19
|
-
query: {
|
20
|
-
users: {
|
21
|
-
findFirst: vi.fn(),
|
22
|
-
},
|
23
|
-
},
|
24
|
-
};
|
25
|
-
|
26
|
-
const mockUserId = 'test-user-id';
|
27
|
-
const userModel = new UserModel(mockDb as any, mockUserId);
|
28
|
-
|
29
|
-
describe('getUserRegistrationDuration', () => {
|
30
|
-
it('should return default values when user not found', async () => {
|
31
|
-
mockDb.query.users.findFirst.mockResolvedValue(null);
|
32
|
-
|
33
|
-
const result = await userModel.getUserRegistrationDuration();
|
34
|
-
|
35
|
-
expect(result).toEqual({
|
36
|
-
createdAt: expect.any(String),
|
37
|
-
duration: 1,
|
38
|
-
updatedAt: expect.any(String),
|
39
|
-
});
|
40
|
-
});
|
41
|
-
|
42
|
-
it('should calculate duration correctly for existing user', async () => {
|
43
|
-
const createdAt = new Date('2024-01-01');
|
44
|
-
mockDb.query.users.findFirst.mockResolvedValue({
|
45
|
-
createdAt,
|
46
|
-
});
|
47
|
-
|
48
|
-
const result = await userModel.getUserRegistrationDuration();
|
49
|
-
|
50
|
-
expect(result).toEqual({
|
51
|
-
createdAt: '2024-01-01',
|
52
|
-
duration: expect.any(Number),
|
53
|
-
updatedAt: expect.any(String),
|
54
|
-
});
|
55
|
-
expect(result.duration).toBeGreaterThan(0);
|
56
|
-
});
|
57
|
-
});
|
58
|
-
});
|
File without changes
|