@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
@@ -1,12 +1,13 @@
1
1
  // @vitest-environment node
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
- import { getTestDBInstance } from '@/database/server/core/dbForTest';
4
+ import { LobeChatDatabase } from '@/database/type';
5
5
 
6
- import { NewInstalledPlugin, userInstalledPlugins, users } from '../../../schemas';
7
- import { PluginModel } from '../plugin';
6
+ import { NewInstalledPlugin, userInstalledPlugins, users } from '../../schemas';
7
+ import { PluginModel } from '../../server/models/plugin';
8
+ import { getTestDB } from './_util';
8
9
 
9
- let serverDB = await getTestDBInstance();
10
+ const serverDB: LobeChatDatabase = await getTestDB();
10
11
 
11
12
  const userId = 'plugin-db';
12
13
  const pluginModel = new PluginModel(serverDB, userId);
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
4
  import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
5
5
  import { getTestDBInstance } from '@/database/server/core/dbForTest';
6
+ import { LobeChatDatabase } from '@/database/type';
6
7
  import { idGenerator } from '@/database/utils/idGenerator';
7
8
 
8
9
  import {
@@ -15,10 +16,11 @@ import {
15
16
  sessions,
16
17
  topics,
17
18
  users,
18
- } from '../../../schemas';
19
- import { SessionModel } from '../session';
19
+ } from '../../schemas';
20
+ import { SessionModel } from '../../server/models/session';
21
+ import { getTestDB } from './_util';
20
22
 
21
- let serverDB = await getTestDBInstance();
23
+ const serverDB: LobeChatDatabase = await getTestDB();
22
24
 
23
25
  const userId = 'session-user';
24
26
  const sessionModel = new SessionModel(serverDB, userId);
@@ -236,8 +238,8 @@ describe('SessionModel', () => {
236
238
  ]);
237
239
 
238
240
  await serverDB.insert(agentsToSessions).values([
239
- { agentId: 'agent-1', sessionId: '1' },
240
- { agentId: 'agent-2', sessionId: '2' },
241
+ { agentId: 'agent-1', sessionId: '1', userId },
242
+ { agentId: 'agent-2', sessionId: '2', userId },
241
243
  ]);
242
244
 
243
245
  const result = await sessionModel.queryByKeyword('hello');
@@ -265,8 +267,8 @@ describe('SessionModel', () => {
265
267
  ]);
266
268
 
267
269
  await serverDB.insert(agentsToSessions).values([
268
- { agentId: 'agent-1', sessionId: '1' },
269
- { agentId: 'agent-2', sessionId: '2' },
270
+ { agentId: 'agent-1', sessionId: '1', userId },
271
+ { agentId: 'agent-2', sessionId: '2', userId },
270
272
  ]);
271
273
 
272
274
  const result = await sessionModel.queryByKeyword('keyword');
@@ -288,9 +290,9 @@ describe('SessionModel', () => {
288
290
  ]);
289
291
 
290
292
  await serverDB.insert(agentsToSessions).values([
291
- { agentId: '1', sessionId: '1' },
292
- { agentId: '2', sessionId: '2' },
293
- { agentId: '3', sessionId: '3' },
293
+ { agentId: '1', sessionId: '1', userId },
294
+ { agentId: '2', sessionId: '2', userId },
295
+ { agentId: '3', sessionId: '3', userId },
294
296
  ]);
295
297
 
296
298
  const result = await sessionModel.queryByKeyword('keyword');
@@ -361,7 +363,8 @@ describe('SessionModel', () => {
361
363
  const result = await sessionModel.batchCreate(sessions);
362
364
 
363
365
  // 断言结果
364
- expect(result.rowCount).toEqual(2);
366
+ // pglite return affectedRows while postgres return rowCount
367
+ expect((result as any).affectedRows || result.rowCount).toEqual(2);
365
368
  });
366
369
 
367
370
  it.skip('should set group to default if group does not exist', async () => {
@@ -391,7 +394,7 @@ describe('SessionModel', () => {
391
394
  .insert(sessions)
392
395
  .values({ id: '1', userId, type: 'agent', title: 'Original Session', pinned: true });
393
396
  await trx.insert(agents).values({ id: 'agent-1', userId, model: 'gpt-3.5-turbo' });
394
- await trx.insert(agentsToSessions).values({ agentId: 'agent-1', sessionId: '1' });
397
+ await trx.insert(agentsToSessions).values({ agentId: 'agent-1', sessionId: '1', userId });
395
398
  });
396
399
 
397
400
  // 调用 duplicate 方法
@@ -801,9 +804,9 @@ describe('SessionModel', () => {
801
804
 
802
805
  // Link agents to sessions
803
806
  await trx.insert(agentsToSessions).values([
804
- { sessionId: '1', agentId: 'a1' },
805
- { sessionId: '2', agentId: 'a2' },
806
- { sessionId: '3', agentId: 'a3' },
807
+ { sessionId: '1', agentId: 'a1', userId },
808
+ { sessionId: '2', agentId: 'a2', userId },
809
+ { sessionId: '3', agentId: 'a3', userId },
807
810
  ]);
808
811
 
809
812
  // Create topics (different counts for ranking)
@@ -863,9 +866,9 @@ describe('SessionModel', () => {
863
866
  ]);
864
867
 
865
868
  await trx.insert(agentsToSessions).values([
866
- { sessionId: '1', agentId: 'a1' },
867
- { sessionId: '2', agentId: 'a2' },
868
- { sessionId: '3', agentId: 'a3' },
869
+ { sessionId: '1', agentId: 'a1', userId },
870
+ { sessionId: '2', agentId: 'a2', userId },
871
+ { sessionId: '3', agentId: 'a3', userId },
869
872
  ]);
870
873
 
871
874
  await trx.insert(topics).values([
@@ -899,8 +902,8 @@ describe('SessionModel', () => {
899
902
  ]);
900
903
 
901
904
  await trx.insert(agentsToSessions).values([
902
- { sessionId: '1', agentId: 'a1' },
903
- { sessionId: '2', agentId: 'a2' },
905
+ { sessionId: '1', agentId: 'a1', userId },
906
+ { sessionId: '2', agentId: 'a2', userId },
904
907
  ]);
905
908
 
906
909
  // No topics created
@@ -2,12 +2,13 @@
2
2
  import { eq } from 'drizzle-orm/expressions';
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
- import { getTestDBInstance } from '@/database/server/core/dbForTest';
5
+ import { LobeChatDatabase } from '@/database/type';
6
6
 
7
- import { sessionGroups, users } from '../../../schemas';
8
- import { SessionGroupModel } from '../sessionGroup';
7
+ import { sessionGroups, users } from '../../schemas';
8
+ import { SessionGroupModel } from '../../server/models/sessionGroup';
9
+ import { getTestDB } from './_util';
9
10
 
10
- let serverDB = await getTestDBInstance();
11
+ const serverDB: LobeChatDatabase = await getTestDB();
11
12
 
12
13
  const userId = 'session-group-model-test-user-id';
13
14
  const sessionGroupModel = new SessionGroupModel(serverDB, userId);
@@ -1,12 +1,13 @@
1
1
  import { eq, inArray } from 'drizzle-orm/expressions';
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
- import { getTestDBInstance } from '@/database/server/core/dbForTest';
4
+ import { LobeChatDatabase } from '@/database/type';
5
5
 
6
- import { messages, sessions, topics, users } from '../../../schemas';
7
- import { CreateTopicParams, TopicModel } from '../topic';
6
+ import { messages, sessions, topics, users } from '../../schemas';
7
+ import { CreateTopicParams, TopicModel } from '../../server/models/topic';
8
+ import { getTestDB } from './_util';
8
9
 
9
- let serverDB = await getTestDBInstance();
10
+ const serverDB: LobeChatDatabase = await getTestDB();
10
11
 
11
12
  const userId = 'topic-user-test';
12
13
  const sessionId = 'topic-session';
@@ -138,6 +138,7 @@ export class DataImporterRepos {
138
138
  shouldInsertSessionAgents.map(({ id }, index) => ({
139
139
  agentId: agentMapArray[index].id,
140
140
  sessionId: sessionIdMap[id],
141
+ userId: this.userId,
141
142
  })),
142
143
  );
143
144
  }
@@ -291,6 +292,7 @@ export class DataImporterRepos {
291
292
  state: msg.pluginState,
292
293
  toolCallId: msg.tool_call_id,
293
294
  type: msg.plugin?.type,
295
+ userId: this.userId,
294
296
  })),
295
297
  );
296
298
  }
@@ -302,6 +304,7 @@ export class DataImporterRepos {
302
304
  translateInserts.map((msg) => ({
303
305
  id: messageIdMap[msg.id],
304
306
  ...msg.extra?.translate,
307
+ userId: this.userId,
305
308
  })),
306
309
  );
307
310
  }
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix */
2
2
  import {
3
3
  boolean,
4
+ index,
4
5
  integer,
5
6
  jsonb,
6
7
  pgTable,
@@ -23,7 +24,9 @@ export const globalFiles = pgTable('global_files', {
23
24
  size: integer('size').notNull(),
24
25
  url: text('url').notNull(),
25
26
  metadata: jsonb('metadata'),
26
-
27
+ creator: text('creator')
28
+ .references(() => users.id, { onDelete: 'set null' })
29
+ .notNull(),
27
30
  createdAt: createdAt(),
28
31
  accessedAt: accessedAt(),
29
32
  });
@@ -31,31 +34,38 @@ export const globalFiles = pgTable('global_files', {
31
34
  export type NewGlobalFile = typeof globalFiles.$inferInsert;
32
35
  export type GlobalFileItem = typeof globalFiles.$inferSelect;
33
36
 
34
- export const files = pgTable('files', {
35
- id: text('id')
36
- .$defaultFn(() => idGenerator('files'))
37
- .primaryKey(),
38
-
39
- userId: text('user_id')
40
- .references(() => users.id, { onDelete: 'cascade' })
41
- .notNull(),
42
- fileType: varchar('file_type', { length: 255 }).notNull(),
43
- fileHash: varchar('file_hash', { length: 64 }).references(() => globalFiles.hashId, {
44
- onDelete: 'no action',
45
- }),
46
- name: text('name').notNull(),
47
- size: integer('size').notNull(),
48
- url: text('url').notNull(),
49
-
50
- metadata: jsonb('metadata'),
51
- chunkTaskId: uuid('chunk_task_id').references(() => asyncTasks.id, { onDelete: 'set null' }),
52
- embeddingTaskId: uuid('embedding_task_id').references(() => asyncTasks.id, {
53
- onDelete: 'set null',
54
- }),
37
+ export const files = pgTable(
38
+ 'files',
39
+ {
40
+ id: text('id')
41
+ .$defaultFn(() => idGenerator('files'))
42
+ .primaryKey(),
55
43
 
56
- ...timestamps,
57
- });
44
+ userId: text('user_id')
45
+ .references(() => users.id, { onDelete: 'cascade' })
46
+ .notNull(),
47
+ fileType: varchar('file_type', { length: 255 }).notNull(),
48
+ fileHash: varchar('file_hash', { length: 64 }).references(() => globalFiles.hashId, {
49
+ onDelete: 'no action',
50
+ }),
51
+ name: text('name').notNull(),
52
+ size: integer('size').notNull(),
53
+ url: text('url').notNull(),
54
+
55
+ metadata: jsonb('metadata'),
56
+ chunkTaskId: uuid('chunk_task_id').references(() => asyncTasks.id, { onDelete: 'set null' }),
57
+ embeddingTaskId: uuid('embedding_task_id').references(() => asyncTasks.id, {
58
+ onDelete: 'set null',
59
+ }),
58
60
 
61
+ ...timestamps,
62
+ },
63
+ (table) => {
64
+ return {
65
+ fileHashIdx: index('file_hash_idx').on(table.fileHash),
66
+ };
67
+ },
68
+ );
59
69
  export type NewFile = typeof files.$inferInsert;
60
70
  export type FileItem = typeof files.$inferSelect;
61
71
 
@@ -97,19 +107,15 @@ export const knowledgeBaseFiles = pgTable(
97
107
  .references(() => files.id, { onDelete: 'cascade' })
98
108
  .notNull(),
99
109
 
100
- // userId: text('user_id')
101
- // .references(() => users.id, { onDelete: 'cascade' })
102
- // .notNull(),
110
+ userId: text('user_id')
111
+ .references(() => users.id, { onDelete: 'cascade' })
112
+ .notNull(),
103
113
 
104
114
  createdAt: createdAt(),
105
115
  },
106
116
  (t) => ({
107
117
  pk: primaryKey({
108
- columns: [
109
- t.knowledgeBaseId,
110
- t.fileId,
111
- // t.userId
112
- ],
118
+ columns: [t.knowledgeBaseId, t.fileId],
113
119
  }),
114
120
  }),
115
121
  );
@@ -95,6 +95,9 @@ export const messagePlugins = pgTable('message_plugins', {
95
95
  identifier: text('identifier'),
96
96
  state: jsonb('state'),
97
97
  error: jsonb('error'),
98
+ userId: text('user_id')
99
+ .references(() => users.id, { onDelete: 'cascade' })
100
+ .notNull(),
98
101
  });
99
102
 
100
103
  export type MessagePluginItem = typeof messagePlugins.$inferSelect;
@@ -107,6 +110,9 @@ export const messageTTS = pgTable('message_tts', {
107
110
  contentMd5: text('content_md5'),
108
111
  fileId: text('file_id').references(() => files.id, { onDelete: 'cascade' }),
109
112
  voice: text('voice'),
113
+ userId: text('user_id')
114
+ .references(() => users.id, { onDelete: 'cascade' })
115
+ .notNull(),
110
116
  });
111
117
 
112
118
  export const messageTranslates = pgTable('message_translates', {
@@ -116,6 +122,9 @@ export const messageTranslates = pgTable('message_translates', {
116
122
  content: text('content'),
117
123
  from: text('from'),
118
124
  to: text('to'),
125
+ userId: text('user_id')
126
+ .references(() => users.id, { onDelete: 'cascade' })
127
+ .notNull(),
119
128
  });
120
129
 
121
130
  // if the message contains a file
@@ -129,6 +138,9 @@ export const messagesFiles = pgTable(
129
138
  messageId: text('message_id')
130
139
  .notNull()
131
140
  .references(() => messages.id, { onDelete: 'cascade' }),
141
+ userId: text('user_id')
142
+ .references(() => users.id, { onDelete: 'cascade' })
143
+ .notNull(),
132
144
  },
133
145
  (t) => ({
134
146
  pk: primaryKey({ columns: [t.fileId, t.messageId] }),
@@ -142,6 +154,9 @@ export const messageQueries = pgTable('message_queries', {
142
154
  .notNull(),
143
155
  rewriteQuery: text('rewrite_query'),
144
156
  userQuery: text('user_query'),
157
+ userId: text('user_id')
158
+ .references(() => users.id, { onDelete: 'cascade' })
159
+ .notNull(),
145
160
  embeddingsId: uuid('embeddings_id').references(() => embeddings.id, { onDelete: 'set null' }),
146
161
  });
147
162
 
@@ -154,6 +169,9 @@ export const messageQueryChunks = pgTable(
154
169
  queryId: uuid('query_id').references(() => messageQueries.id, { onDelete: 'cascade' }),
155
170
  chunkId: uuid('chunk_id').references(() => chunks.id, { onDelete: 'cascade' }),
156
171
  similarity: numeric('similarity', { precision: 6, scale: 5 }),
172
+ userId: text('user_id')
173
+ .references(() => users.id, { onDelete: 'cascade' })
174
+ .notNull(),
157
175
  },
158
176
  (t) => ({
159
177
  pk: primaryKey({ columns: [t.chunkId, t.messageId, t.queryId] }),
@@ -168,6 +186,9 @@ export const messageChunks = pgTable(
168
186
  {
169
187
  messageId: text('message_id').references(() => messages.id, { onDelete: 'cascade' }),
170
188
  chunkId: uuid('chunk_id').references(() => chunks.id, { onDelete: 'cascade' }),
189
+ userId: text('user_id')
190
+ .references(() => users.id, { onDelete: 'cascade' })
191
+ .notNull(),
171
192
  },
172
193
  (t) => ({
173
194
  pk: primaryKey({ columns: [t.chunkId, t.messageId] }),
@@ -11,6 +11,7 @@ import { messages, messagesFiles } from './message';
11
11
  import { chunks, unstructuredChunks } from './rag';
12
12
  import { sessionGroups, sessions } from './session';
13
13
  import { threads, topics } from './topic';
14
+ import { users } from './user';
14
15
 
15
16
  export const agentsToSessions = pgTable(
16
17
  'agents_to_sessions',
@@ -21,6 +22,9 @@ export const agentsToSessions = pgTable(
21
22
  sessionId: text('session_id')
22
23
  .notNull()
23
24
  .references(() => sessions.id, { onDelete: 'cascade' }),
25
+ userId: text('user_id')
26
+ .references(() => users.id, { onDelete: 'cascade' })
27
+ .notNull(),
24
28
  },
25
29
  (t) => ({
26
30
  pk: primaryKey({ columns: [t.agentId, t.sessionId] }),
@@ -36,6 +40,9 @@ export const filesToSessions = pgTable(
36
40
  sessionId: text('session_id')
37
41
  .notNull()
38
42
  .references(() => sessions.id, { onDelete: 'cascade' }),
43
+ userId: text('user_id')
44
+ .references(() => users.id, { onDelete: 'cascade' })
45
+ .notNull(),
39
46
  },
40
47
  (t) => ({
41
48
  pk: primaryKey({ columns: [t.fileId, t.sessionId] }),
@@ -48,6 +55,9 @@ export const fileChunks = pgTable(
48
55
  fileId: varchar('file_id').references(() => files.id, { onDelete: 'cascade' }),
49
56
  chunkId: uuid('chunk_id').references(() => chunks.id, { onDelete: 'cascade' }),
50
57
  createdAt: createdAt(),
58
+ userId: text('user_id')
59
+ .references(() => users.id, { onDelete: 'cascade' })
60
+ .notNull(),
51
61
  },
52
62
  (t) => ({
53
63
  pk: primaryKey({ columns: [t.fileId, t.chunkId] }),
@@ -16,9 +16,11 @@ import {
16
16
  users,
17
17
  } from '@/database/schemas';
18
18
  import { getTestDBInstance } from '@/database/server/core/dbForTest';
19
+ import { LobeChatDatabase } from '@/database/type';
19
20
  import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
20
21
 
21
22
  let serverDB = await getTestDBInstance();
23
+
22
24
  let nextAuthAdapter = LobeNextAuthDbAdapter(serverDB);
23
25
 
24
26
  const userId = 'user-db';
@@ -1,15 +1,17 @@
1
+ import { TRPCError } from '@trpc/server';
1
2
  import dayjs from 'dayjs';
2
3
  import { eq } from 'drizzle-orm/expressions';
3
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
5
 
5
6
  import { INBOX_SESSION_ID } from '@/const/session';
6
7
  import { getTestDBInstance } from '@/database/server/core/dbForTest';
8
+ import { LobeChatDatabase } from '@/database/type';
7
9
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
8
10
  import { UserGuide, UserPreference } from '@/types/user';
9
11
 
10
12
  import { UserSettingsItem, userSettings, users } from '../../../schemas';
11
13
  import { SessionModel } from '../session';
12
- import { UserModel } from '../user';
14
+ import { UserModel, UserNotFoundError } from '../user';
13
15
 
14
16
  let serverDB = await getTestDBInstance();
15
17
 
@@ -408,3 +410,13 @@ describe('UserModel', () => {
408
410
  });
409
411
  });
410
412
  });
413
+
414
+ describe('UserNotFoundError', () => {
415
+ it('should extend TRPCError with correct code and message', () => {
416
+ const error = new UserNotFoundError();
417
+
418
+ expect(error).toBeInstanceOf(TRPCError);
419
+ expect(error.code).toBe('UNAUTHORIZED');
420
+ expect(error.message).toBe('user not found');
421
+ });
422
+ });
@@ -31,7 +31,11 @@ export class ChunkModel {
31
31
 
32
32
  const result = await trx.insert(chunks).values(params).returning();
33
33
 
34
- const fileChunksData = result.map((chunk) => ({ chunkId: chunk.id, fileId }));
34
+ const fileChunksData = result.map((chunk) => ({
35
+ chunkId: chunk.id,
36
+ fileId,
37
+ userId: this.userId,
38
+ }));
35
39
 
36
40
  if (fileChunksData.length > 0) {
37
41
  await trx.insert(fileChunks).values(fileChunksData);
@@ -33,6 +33,7 @@ export class FileModel {
33
33
  const result = await this.db.transaction(async (trx) => {
34
34
  if (insertToGlobalFiles) {
35
35
  await trx.insert(globalFiles).values({
36
+ creator: this.userId,
36
37
  fileType: params.fileType,
37
38
  hashId: params.fileHash!,
38
39
  metadata: params.metadata,
@@ -49,9 +50,11 @@ export class FileModel {
49
50
  const item = result[0];
50
51
 
51
52
  if (params.knowledgeBaseId) {
52
- await trx
53
- .insert(knowledgeBaseFiles)
54
- .values({ fileId: item.id, knowledgeBaseId: params.knowledgeBaseId });
53
+ await trx.insert(knowledgeBaseFiles).values({
54
+ fileId: item.id,
55
+ knowledgeBaseId: params.knowledgeBaseId,
56
+ userId: this.userId,
57
+ });
55
58
  }
56
59
 
57
60
  return item;
@@ -21,6 +21,7 @@ import {
21
21
  CreateMessageParams,
22
22
  MessageItem,
23
23
  ModelRankItem,
24
+ NewMessageQueryParams,
24
25
  UpdateMessageParams,
25
26
  } from '@/types/message';
26
27
  import { merge } from '@/utils/merge';
@@ -28,7 +29,6 @@ import { today } from '@/utils/time';
28
29
 
29
30
  import {
30
31
  MessagePluginItem,
31
- NewMessageQuery,
32
32
  chunks,
33
33
  embeddings,
34
34
  fileChunks,
@@ -458,13 +458,14 @@ export class MessageModel {
458
458
  state: pluginState,
459
459
  toolCallId: message.tool_call_id,
460
460
  type: plugin?.type,
461
+ userId: this.userId,
461
462
  });
462
463
  }
463
464
 
464
465
  if (files && files.length > 0) {
465
466
  await trx
466
467
  .insert(messagesFiles)
467
- .values(files.map((file) => ({ fileId: file, messageId: id })));
468
+ .values(files.map((file) => ({ fileId: file, messageId: id, userId: this.userId })));
468
469
  }
469
470
 
470
471
  if (fileChunks && fileChunks.length > 0 && ragQueryId) {
@@ -474,6 +475,7 @@ export class MessageModel {
474
475
  messageId: id,
475
476
  queryId: ragQueryId,
476
477
  similarity: chunk.similarity?.toString(),
478
+ userId: this.userId,
477
479
  })),
478
480
  );
479
481
  }
@@ -491,8 +493,11 @@ export class MessageModel {
491
493
  return this.db.insert(messages).values(messagesToInsert);
492
494
  };
493
495
 
494
- createMessageQuery = async (params: NewMessageQuery) => {
495
- const result = await this.db.insert(messageQueries).values(params).returning();
496
+ createMessageQuery = async (params: NewMessageQueryParams) => {
497
+ const result = await this.db
498
+ .insert(messageQueries)
499
+ .values({ ...params, userId: this.userId })
500
+ .returning();
496
501
 
497
502
  return result[0];
498
503
  };
@@ -504,7 +509,9 @@ export class MessageModel {
504
509
  if (imageList && imageList.length > 0) {
505
510
  await trx
506
511
  .insert(messagesFiles)
507
- .values(imageList.map((file) => ({ fileId: file.id, messageId: id })));
512
+ .values(
513
+ imageList.map((file) => ({ fileId: file.id, messageId: id, userId: this.userId })),
514
+ );
508
515
  }
509
516
 
510
517
  return trx
@@ -547,7 +554,7 @@ export class MessageModel {
547
554
 
548
555
  // If the message does not exist in the translate table, insert it
549
556
  if (!result) {
550
- return this.db.insert(messageTranslates).values({ ...translate, id });
557
+ return this.db.insert(messageTranslates).values({ ...translate, id, userId: this.userId });
551
558
  }
552
559
 
553
560
  // or just update the existing one
@@ -561,9 +568,13 @@ export class MessageModel {
561
568
 
562
569
  // If the message does not exist in the translate table, insert it
563
570
  if (!result) {
564
- return this.db
565
- .insert(messageTTS)
566
- .values({ contentMd5: tts.contentMd5, fileId: tts.file, id, voice: tts.voice });
571
+ return this.db.insert(messageTTS).values({
572
+ contentMd5: tts.contentMd5,
573
+ fileId: tts.file,
574
+ id,
575
+ userId: this.userId,
576
+ voice: tts.voice,
577
+ });
567
578
  }
568
579
 
569
580
  // or just update the existing one
@@ -618,13 +629,19 @@ export class MessageModel {
618
629
  .where(and(eq(messages.userId, this.userId), inArray(messages.id, ids)));
619
630
 
620
631
  deleteMessageTranslate = async (id: string) =>
621
- this.db.delete(messageTranslates).where(and(eq(messageTranslates.id, id)));
632
+ this.db
633
+ .delete(messageTranslates)
634
+ .where(and(eq(messageTranslates.id, id), eq(messageTranslates.userId, this.userId)));
622
635
 
623
636
  deleteMessageTTS = async (id: string) =>
624
- this.db.delete(messageTTS).where(and(eq(messageTTS.id, id)));
637
+ this.db
638
+ .delete(messageTTS)
639
+ .where(and(eq(messageTTS.id, id), eq(messageTTS.userId, this.userId)));
625
640
 
626
641
  deleteMessageQuery = async (id: string) =>
627
- this.db.delete(messageQueries).where(and(eq(messageQueries.id, id)));
642
+ this.db
643
+ .delete(messageQueries)
644
+ .where(and(eq(messageQueries.id, id), eq(messageQueries.userId, this.userId)));
628
645
 
629
646
  deleteMessagesBySession = async (sessionId?: string | null, topicId?: string | null) =>
630
647
  this.db
@@ -220,6 +220,7 @@ export class SessionModel {
220
220
  await trx.insert(agentsToSessions).values({
221
221
  agentId: newAgents[0].id,
222
222
  sessionId: id,
223
+ userId: this.userId,
223
224
  });
224
225
 
225
226
  return result[0];
@@ -1,10 +1,12 @@
1
- import { Form, type FormItemProps } from '@lobehub/ui';
1
+ import { Form, type FormItemProps, Icon } from '@lobehub/ui';
2
2
  import { Button, Segmented, Switch } from 'antd';
3
+ import { CopyIcon } from 'lucide-react';
3
4
  import { memo, useState } from 'react';
4
5
  import { useTranslation } from 'react-i18next';
5
6
  import { Flexbox } from 'react-layout-kit';
6
7
 
7
8
  import { FORM_STYLE } from '@/const/layoutTokens';
9
+ import { useImgToClipboard } from '@/hooks/useImgToClipboard';
8
10
  import { useIsMobile } from '@/hooks/useIsMobile';
9
11
  import { ImageType, imageTypeOptions, useScreenshot } from '@/hooks/useScreenshot';
10
12
  import { useSessionStore } from '@/store/session';
@@ -32,7 +34,9 @@ const ShareImage = memo<{ mobile?: boolean }>(({ mobile }) => {
32
34
  title: currentAgentTitle,
33
35
  width: mobile ? 720 : undefined,
34
36
  });
35
-
37
+ const { loading: copyLoading, onCopy } = useImgToClipboard({
38
+ width: mobile ? 720 : undefined,
39
+ });
36
40
  const settings: FormItemProps[] = [
37
41
  {
38
42
  children: <Switch />,
@@ -66,15 +70,27 @@ const ShareImage = memo<{ mobile?: boolean }>(({ mobile }) => {
66
70
  const isMobile = useIsMobile();
67
71
 
68
72
  const button = (
69
- <Button
70
- block
71
- loading={loading}
72
- onClick={onDownload}
73
- size={isMobile ? undefined : 'large'}
74
- type={'primary'}
75
- >
76
- {t('shareModal.download')}
77
- </Button>
73
+ <>
74
+ <Button
75
+ block
76
+ icon={<Icon icon={CopyIcon} />}
77
+ loading={copyLoading}
78
+ onClick={() => onCopy()}
79
+ size={isMobile ? undefined : 'large'}
80
+ type={'primary'}
81
+ >
82
+ {t('copy', { ns: 'common' })}
83
+ </Button>
84
+ <Button
85
+ block
86
+ loading={loading}
87
+ onClick={onDownload}
88
+ size={isMobile ? undefined : 'large'}
89
+ variant={'filled'}
90
+ >
91
+ {t('shareModal.download')}
92
+ </Button>
93
+ </>
78
94
  );
79
95
 
80
96
  return (