@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/developer/database-schema.dbml +16 -0
  4. package/package.json +3 -3
  5. package/src/database/client/db.ts +14 -8
  6. package/src/database/client/migrations.json +62 -0
  7. package/src/database/migrations/0017_add_user_id_to_tables.sql +225 -0
  8. package/src/database/migrations/meta/0017_snapshot.json +3858 -0
  9. package/src/database/migrations/meta/_journal.json +7 -0
  10. package/src/database/{server/models → models}/__tests__/_test_template.ts +2 -2
  11. package/src/database/models/__tests__/_util.ts +12 -0
  12. package/src/database/{server/models → models}/__tests__/agent.test.ts +6 -5
  13. package/src/database/{server/models → models}/__tests__/aiModel.test.ts +5 -4
  14. package/src/database/{server/models → models}/__tests__/aiProvider.test.ts +5 -4
  15. package/src/database/{server/models → models}/__tests__/asyncTask.test.ts +5 -4
  16. package/src/database/{server/models → models}/__tests__/chunk.test.ts +25 -21
  17. package/src/database/{server/models → models}/__tests__/file.test.ts +19 -5
  18. package/src/database/{server/models → models}/__tests__/knowledgeBase.test.ts +9 -4
  19. package/src/database/{server/models → models}/__tests__/message.test.ts +625 -29
  20. package/src/database/{server/models → models}/__tests__/plugin.test.ts +5 -4
  21. package/src/database/{server/models → models}/__tests__/session.test.ts +23 -20
  22. package/src/database/{server/models → models}/__tests__/sessionGroup.test.ts +5 -4
  23. package/src/database/{server/models → models}/__tests__/topic.test.ts +5 -4
  24. package/src/database/repositories/dataImporter/index.ts +3 -0
  25. package/src/database/schemas/file.ts +38 -32
  26. package/src/database/schemas/message.ts +21 -0
  27. package/src/database/schemas/relations.ts +10 -0
  28. package/src/database/server/models/__tests__/nextauth.test.ts +2 -0
  29. package/src/database/server/models/__tests__/user.test.ts +13 -1
  30. package/src/database/server/models/chunk.ts +5 -1
  31. package/src/database/server/models/file.ts +6 -3
  32. package/src/database/server/models/message.ts +29 -12
  33. package/src/database/server/models/session.ts +1 -0
  34. package/src/features/ShareModal/ShareImage/index.tsx +27 -11
  35. package/src/hooks/useImgToClipboard.ts +29 -0
  36. package/src/hooks/useScreenshot.ts +53 -40
  37. package/src/services/file/client.test.ts +2 -1
  38. package/src/services/message/client.test.ts +3 -3
  39. package/src/services/session/client.test.ts +5 -3
  40. package/src/types/message/base.ts +7 -0
  41. package/vitest.server.config.ts +1 -1
  42. package/src/database/server/models/user.test.ts +0 -58
  43. /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
- let screenshotFn: any;
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.insert(agentsToSessions).values([{ agentId: mockAgentId, sessionId: mockSessionId }]);
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
+ }
@@ -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
- });