@lobehub/chat 1.35.9 → 1.35.11

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 (55) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/changelog/v1.json +14 -0
  3. package/package.json +2 -2
  4. package/src/app/(main)/repos/[id]/@menu/default.tsx +2 -1
  5. package/src/app/(main)/repos/[id]/page.tsx +2 -1
  6. package/src/database/schemas/topic.ts +3 -1
  7. package/src/database/server/core/dbForTest.ts +4 -6
  8. package/src/database/server/models/__tests__/_test_template.ts +3 -9
  9. package/src/database/server/models/__tests__/agent.test.ts +2 -8
  10. package/src/database/server/models/__tests__/asyncTask.test.ts +1 -7
  11. package/src/database/server/models/__tests__/chunk.test.ts +155 -16
  12. package/src/database/server/models/__tests__/file.test.ts +123 -15
  13. package/src/database/server/models/__tests__/knowledgeBase.test.ts +6 -12
  14. package/src/database/server/models/__tests__/message.test.ts +230 -7
  15. package/src/database/server/models/__tests__/nextauth.test.ts +1 -7
  16. package/src/database/server/models/__tests__/plugin.test.ts +1 -7
  17. package/src/database/server/models/__tests__/session.test.ts +169 -11
  18. package/src/database/server/models/__tests__/sessionGroup.test.ts +2 -8
  19. package/src/database/server/models/__tests__/topic.test.ts +1 -7
  20. package/src/database/server/models/__tests__/user.test.ts +55 -20
  21. package/src/database/server/models/_template.ts +10 -8
  22. package/src/database/server/models/agent.ts +17 -13
  23. package/src/database/server/models/asyncTask.ts +11 -9
  24. package/src/database/server/models/chunk.ts +19 -14
  25. package/src/database/server/models/embedding.ts +10 -8
  26. package/src/database/server/models/file.ts +19 -17
  27. package/src/database/server/models/knowledgeBase.ts +14 -12
  28. package/src/database/server/models/message.ts +36 -34
  29. package/src/database/server/models/plugin.ts +10 -8
  30. package/src/database/server/models/session.ts +23 -64
  31. package/src/database/server/models/sessionGroup.ts +11 -9
  32. package/src/database/server/models/thread.ts +11 -9
  33. package/src/database/server/models/topic.ts +19 -22
  34. package/src/database/server/models/user.ts +96 -84
  35. package/src/database/type.ts +7 -0
  36. package/src/libs/next-auth/adapter/index.ts +10 -10
  37. package/src/libs/trpc/async/asyncAuth.ts +2 -1
  38. package/src/server/routers/async/file.ts +5 -4
  39. package/src/server/routers/async/ragEval.ts +4 -3
  40. package/src/server/routers/lambda/_template.ts +2 -1
  41. package/src/server/routers/lambda/agent.ts +6 -5
  42. package/src/server/routers/lambda/chunk.ts +5 -5
  43. package/src/server/routers/lambda/file.ts +4 -3
  44. package/src/server/routers/lambda/knowledgeBase.ts +2 -1
  45. package/src/server/routers/lambda/message.ts +4 -2
  46. package/src/server/routers/lambda/plugin.ts +4 -2
  47. package/src/server/routers/lambda/ragEval.ts +2 -1
  48. package/src/server/routers/lambda/session.ts +4 -3
  49. package/src/server/routers/lambda/sessionGroup.ts +2 -1
  50. package/src/server/routers/lambda/thread.ts +3 -2
  51. package/src/server/routers/lambda/topic.ts +4 -2
  52. package/src/server/routers/lambda/user.ts +10 -9
  53. package/src/server/services/chunk/index.ts +3 -2
  54. package/src/server/services/nextAuthUser/index.ts +3 -3
  55. package/src/server/services/user/index.ts +7 -6
@@ -2,10 +2,16 @@ import { eq } from 'drizzle-orm';
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
4
  import { getTestDBInstance } from '@/database/server/core/dbForTest';
5
+ import { uuid } from '@/utils/uuid';
5
6
 
6
7
  import {
8
+ chunks,
9
+ embeddings,
10
+ fileChunks,
7
11
  files,
8
12
  messagePlugins,
13
+ messageQueries,
14
+ messageQueryChunks,
9
15
  messageTTS,
10
16
  messageTranslates,
11
17
  messages,
@@ -15,17 +21,12 @@ import {
15
21
  users,
16
22
  } from '../../../schemas';
17
23
  import { MessageModel } from '../message';
24
+ import { codeEmbedding } from './fixtures/embedding';
18
25
 
19
26
  let serverDB = await getTestDBInstance();
20
27
 
21
- vi.mock('@/database/server/core/db', async () => ({
22
- get serverDB() {
23
- return serverDB;
24
- },
25
- }));
26
-
27
28
  const userId = 'message-db';
28
- const messageModel = new MessageModel(userId);
29
+ const messageModel = new MessageModel(serverDB, userId);
29
30
 
30
31
  beforeEach(async () => {
31
32
  // 在每个测试用例之前,清空表
@@ -273,6 +274,93 @@ describe('MessageModel', () => {
273
274
  const result3 = await messageModel.query({ current: 2, pageSize: 2 });
274
275
  expect(result3).toHaveLength(0);
275
276
  });
277
+
278
+ // 补充测试复杂查询场景
279
+ it('should handle complex query with multiple joins and file chunks', async () => {
280
+ await serverDB.transaction(async (trx) => {
281
+ const chunk1Id = uuid();
282
+ const query1Id = uuid();
283
+ // 创建基础消息
284
+ await trx.insert(messages).values({
285
+ id: 'msg1',
286
+ userId,
287
+ role: 'user',
288
+ content: 'test message',
289
+ createdAt: new Date('2023-01-01'),
290
+ });
291
+
292
+ // 创建文件
293
+ await trx.insert(files).values([
294
+ {
295
+ id: 'file1',
296
+ userId,
297
+ name: 'test.txt',
298
+ url: 'test-url',
299
+ fileType: 'text/plain',
300
+ size: 100,
301
+ },
302
+ ]);
303
+
304
+ // 创建文件块
305
+ await trx.insert(chunks).values({
306
+ id: chunk1Id,
307
+ text: 'chunk content',
308
+ });
309
+
310
+ // 关联消息和文件
311
+ await trx.insert(messagesFiles).values({
312
+ messageId: 'msg1',
313
+ fileId: 'file1',
314
+ });
315
+
316
+ // 创建文件块关联
317
+ await trx.insert(fileChunks).values({
318
+ fileId: 'file1',
319
+ chunkId: chunk1Id,
320
+ });
321
+
322
+ // 创建消息查询
323
+ await trx.insert(messageQueries).values({
324
+ id: query1Id,
325
+ messageId: 'msg1',
326
+ userQuery: 'original query',
327
+ rewriteQuery: 'rewritten query',
328
+ });
329
+
330
+ // 创建消息查询块关联
331
+ await trx.insert(messageQueryChunks).values({
332
+ messageId: 'msg1',
333
+ queryId: query1Id,
334
+ chunkId: chunk1Id,
335
+ similarity: '0.95',
336
+ });
337
+ });
338
+
339
+ const result = await messageModel.query();
340
+
341
+ expect(result).toHaveLength(1);
342
+ expect(result[0].chunksList).toHaveLength(1);
343
+ expect(result[0].chunksList[0]).toMatchObject({
344
+ text: 'chunk content',
345
+ similarity: 0.95,
346
+ });
347
+ });
348
+
349
+ it('should return empty arrays for files and chunks if none exist', async () => {
350
+ await serverDB.insert(messages).values({
351
+ id: 'msg1',
352
+ userId,
353
+ role: 'user',
354
+ content: 'test message',
355
+ });
356
+
357
+ const result = await messageModel.query();
358
+
359
+ expect(result).toHaveLength(1);
360
+ expect(result[0].fileList).toEqual([]);
361
+ expect(result[0].imageList).toEqual([]);
362
+ expect(result[0].chunksList).toEqual([]);
363
+ });
276
364
  });
277
365
 
278
366
  describe('queryAll', () => {
@@ -1021,4 +1109,139 @@ describe('MessageModel', () => {
1021
1109
  expect(result).toBe(2);
1022
1110
  });
1023
1111
  });
1112
+
1113
+ describe('findMessageQueriesById', () => {
1114
+ it('should return undefined for non-existent message query', async () => {
1115
+ const result = await messageModel.findMessageQueriesById('non-existent-id');
1116
+ expect(result).toBeUndefined();
1117
+ });
1118
+
1119
+ it('should return message query with embeddings', async () => {
1120
+ const query1Id = uuid();
1121
+ const embeddings1Id = uuid();
1122
+
1123
+ await serverDB.transaction(async (trx) => {
1124
+ await trx.insert(messages).values({ id: 'msg1', userId, role: 'user', content: 'abc' });
1125
+
1126
+ await trx.insert(embeddings).values({
1127
+ id: embeddings1Id,
1128
+ embeddings: codeEmbedding,
1129
+ });
1130
+
1131
+ await trx.insert(messageQueries).values({
1132
+ id: query1Id,
1133
+ messageId: 'msg1',
1134
+ userQuery: 'test query',
1135
+ rewriteQuery: 'rewritten query',
1136
+ embeddingsId: embeddings1Id,
1137
+ });
1138
+ });
1139
+
1140
+ const result = await messageModel.findMessageQueriesById('msg1');
1141
+
1142
+ expect(result).toBeDefined();
1143
+ expect(result).toMatchObject({
1144
+ id: query1Id,
1145
+ userQuery: 'test query',
1146
+ rewriteQuery: 'rewritten query',
1147
+ embeddings: codeEmbedding,
1148
+ });
1149
+ });
1150
+ });
1151
+
1152
+ describe('deleteMessagesBySession', () => {
1153
+ it('should delete messages by session ID', async () => {
1154
+ await serverDB.insert(sessions).values([
1155
+ { id: 'session1', userId },
1156
+ { id: 'session2', userId },
1157
+ ]);
1158
+
1159
+ await serverDB.insert(messages).values([
1160
+ {
1161
+ id: '1',
1162
+ userId,
1163
+ sessionId: 'session1',
1164
+ role: 'user',
1165
+ content: 'message 1',
1166
+ },
1167
+ {
1168
+ id: '2',
1169
+ userId,
1170
+ sessionId: 'session1',
1171
+ role: 'assistant',
1172
+ content: 'message 2',
1173
+ },
1174
+ {
1175
+ id: '3',
1176
+ userId,
1177
+ sessionId: 'session2',
1178
+ role: 'user',
1179
+ content: 'message 3',
1180
+ },
1181
+ ]);
1182
+
1183
+ await messageModel.deleteMessagesBySession('session1');
1184
+
1185
+ const remainingMessages = await serverDB
1186
+ .select()
1187
+ .from(messages)
1188
+ .where(eq(messages.userId, userId));
1189
+
1190
+ expect(remainingMessages).toHaveLength(1);
1191
+ expect(remainingMessages[0].id).toBe('3');
1192
+ });
1193
+
1194
+ it('should delete messages by session ID and topic ID', async () => {
1195
+ await serverDB.insert(sessions).values([{ id: 'session1', userId }]);
1196
+ await serverDB.insert(topics).values([
1197
+ { id: 'topic1', sessionId: 'session1', userId },
1198
+ { id: 'topic2', sessionId: 'session1', userId },
1199
+ ]);
1200
+
1201
+ await serverDB.insert(messages).values([
1202
+ {
1203
+ id: '1',
1204
+ userId,
1205
+ sessionId: 'session1',
1206
+ topicId: 'topic1',
1207
+ role: 'user',
1208
+ content: 'message 1',
1209
+ },
1210
+ {
1211
+ id: '2',
1212
+ userId,
1213
+ sessionId: 'session1',
1214
+ topicId: 'topic2',
1215
+ role: 'assistant',
1216
+ content: 'message 2',
1217
+ },
1218
+ ]);
1219
+
1220
+ await messageModel.deleteMessagesBySession('session1', 'topic1');
1221
+
1222
+ const remainingMessages = await serverDB
1223
+ .select()
1224
+ .from(messages)
1225
+ .where(eq(messages.userId, userId));
1226
+
1227
+ expect(remainingMessages).toHaveLength(1);
1228
+ expect(remainingMessages[0].id).toBe('2');
1229
+ });
1230
+ });
1231
+
1232
+ describe('genId', () => {
1233
+ it('should generate unique message IDs', () => {
1234
+ const model = new MessageModel(serverDB, userId);
1235
+ // @ts-ignore - accessing private method for testing
1236
+ const id1 = model.genId();
1237
+ // @ts-ignore - accessing private method for testing
1238
+ const id2 = model.genId();
1239
+
1240
+ expect(id1).toHaveLength(18);
1241
+ expect(id2).toHaveLength(18);
1242
+ expect(id1).not.toBe(id2);
1243
+ expect(id1).toMatch(/^msg_/);
1244
+ expect(id2).toMatch(/^msg_/);
1245
+ });
1246
+ });
1024
1247
  });
@@ -8,7 +8,6 @@ import type {
8
8
  import { eq } from 'drizzle-orm';
9
9
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
10
10
 
11
- import { getTestDBInstance } from '@/database/server/core/dbForTest';
12
11
  import {
13
12
  nextauthAccounts,
14
13
  nextauthAuthenticators,
@@ -16,17 +15,12 @@ import {
16
15
  nextauthVerificationTokens,
17
16
  users,
18
17
  } from '@/database/schemas';
18
+ import { getTestDBInstance } from '@/database/server/core/dbForTest';
19
19
  import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
20
20
 
21
21
  let serverDB = await getTestDBInstance();
22
22
  let nextAuthAdapter = LobeNextAuthDbAdapter(serverDB);
23
23
 
24
- vi.mock('@/database/server/core/db', async () => ({
25
- get serverDB() {
26
- return serverDB;
27
- },
28
- }));
29
-
30
24
  const userId = 'user-db';
31
25
  const user: AdapterUser = {
32
26
  id: userId,
@@ -8,14 +8,8 @@ import { PluginModel } from '../plugin';
8
8
 
9
9
  let serverDB = await getTestDBInstance();
10
10
 
11
- vi.mock('@/database/server/core/db', async () => ({
12
- get serverDB() {
13
- return serverDB;
14
- },
15
- }));
16
-
17
11
  const userId = 'plugin-db';
18
- const pluginModel = new PluginModel(userId);
12
+ const pluginModel = new PluginModel(serverDB, userId);
19
13
 
20
14
  beforeEach(async () => {
21
15
  await serverDB.transaction(async (trx) => {
@@ -1,7 +1,9 @@
1
- import { eq, inArray } from 'drizzle-orm';
1
+ import { and, eq, inArray } from 'drizzle-orm';
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
+ import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
4
5
  import { getTestDBInstance } from '@/database/server/core/dbForTest';
6
+ import { idGenerator } from '@/database/utils/idGenerator';
5
7
 
6
8
  import {
7
9
  NewSession,
@@ -14,19 +16,12 @@ import {
14
16
  topics,
15
17
  users,
16
18
  } from '../../../schemas';
17
- import { idGenerator } from '@/database/utils/idGenerator';
18
19
  import { SessionModel } from '../session';
19
20
 
20
21
  let serverDB = await getTestDBInstance();
21
22
 
22
- vi.mock('@/database/server/core/db', async () => ({
23
- get serverDB() {
24
- return serverDB;
25
- },
26
- }));
27
-
28
23
  const userId = 'session-user';
29
- const sessionModel = new SessionModel(userId);
24
+ const sessionModel = new SessionModel(serverDB, userId);
30
25
 
31
26
  beforeEach(async () => {
32
27
  await serverDB.delete(users);
@@ -259,7 +254,13 @@ describe('SessionModel', () => {
259
254
  ]);
260
255
 
261
256
  await serverDB.insert(agents).values([
262
- { id: 'agent-1', userId, model: 'gpt-3.5-turbo', title: 'Agent 1', description: 'Description with Keyword' },
257
+ {
258
+ id: 'agent-1',
259
+ userId,
260
+ model: 'gpt-3.5-turbo',
261
+ title: 'Agent 1',
262
+ description: 'Description with Keyword',
263
+ },
263
264
  { id: 'agent-2', userId, model: 'gpt-4', title: 'Agent 2' },
264
265
  ]);
265
266
 
@@ -338,7 +339,7 @@ describe('SessionModel', () => {
338
339
  });
339
340
  });
340
341
 
341
- describe.skip('batchCreate', () => {
342
+ describe('batchCreate', () => {
342
343
  it('should batch create sessions', async () => {
343
344
  // 调用 batchCreate 方法
344
345
  const sessions: NewSession[] = [
@@ -624,4 +625,161 @@ describe('SessionModel', () => {
624
625
  expect(await serverDB.select().from(sessions).where(eq(sessions.id, '3'))).toHaveLength(1);
625
626
  });
626
627
  });
628
+
629
+ // 在原有的 describe('SessionModel') 中添加以下测试套件
630
+
631
+ describe('createInbox', () => {
632
+ it('should create inbox session if not exists', async () => {
633
+ const inbox = await sessionModel.createInbox();
634
+
635
+ expect(inbox).toBeDefined();
636
+ expect(inbox?.slug).toBe('inbox');
637
+
638
+ // verify agent config
639
+ const session = await sessionModel.findByIdOrSlug('inbox');
640
+ expect(session?.agent).toBeDefined();
641
+ expect(session?.agent.model).toBe(DEFAULT_AGENT_CONFIG.model);
642
+ });
643
+
644
+ it('should not create duplicate inbox session', async () => {
645
+ // Create first inbox
646
+ await sessionModel.createInbox();
647
+
648
+ // Try to create another inbox
649
+ const duplicateInbox = await sessionModel.createInbox();
650
+
651
+ // Should return undefined as inbox already exists
652
+ expect(duplicateInbox).toBeUndefined();
653
+
654
+ // Verify only one inbox exists
655
+ const sessions = await serverDB.query.sessions.findMany();
656
+
657
+ const inboxSessions = sessions.filter((s) => s.slug === 'inbox');
658
+ expect(inboxSessions).toHaveLength(1);
659
+ });
660
+ });
661
+
662
+ describe('deleteAll', () => {
663
+ it('should delete all sessions for current user', async () => {
664
+ // Create test data
665
+ await serverDB.insert(sessions).values([
666
+ { id: '1', userId },
667
+ { id: '2', userId },
668
+ { id: '3', userId },
669
+ ]);
670
+
671
+ // Create sessions for another user that should not be deleted
672
+ await serverDB.insert(users).values([{ id: 'other-user' }]);
673
+ await serverDB.insert(sessions).values([
674
+ { id: '4', userId: 'other-user' },
675
+ { id: '5', userId: 'other-user' },
676
+ ]);
677
+
678
+ await sessionModel.deleteAll();
679
+
680
+ // Verify all sessions for current user are deleted
681
+ const remainingSessions = await serverDB
682
+ .select()
683
+ .from(sessions)
684
+ .where(eq(sessions.userId, userId));
685
+ expect(remainingSessions).toHaveLength(0);
686
+
687
+ // Verify other user's sessions are not deleted
688
+ const otherUserSessions = await serverDB
689
+ .select()
690
+ .from(sessions)
691
+ .where(eq(sessions.userId, 'other-user'));
692
+ expect(otherUserSessions).toHaveLength(2);
693
+ });
694
+
695
+ it('should delete associated data when deleting all sessions', async () => {
696
+ // Create test data with associated records
697
+ await serverDB.transaction(async (trx) => {
698
+ await trx.insert(sessions).values([
699
+ { id: '1', userId },
700
+ { id: '2', userId },
701
+ ]);
702
+
703
+ await trx.insert(topics).values([
704
+ { id: 't1', sessionId: '1', userId },
705
+ { id: 't2', sessionId: '2', userId },
706
+ ]);
707
+
708
+ await trx.insert(messages).values([
709
+ { id: 'm1', sessionId: '1', userId, role: 'user' },
710
+ { id: 'm2', sessionId: '2', userId, role: 'assistant' },
711
+ ]);
712
+ });
713
+
714
+ await sessionModel.deleteAll();
715
+
716
+ // Verify all associated data is deleted
717
+ const remainingTopics = await serverDB.select().from(topics).where(eq(topics.userId, userId));
718
+ expect(remainingTopics).toHaveLength(0);
719
+
720
+ const remainingMessages = await serverDB
721
+ .select()
722
+ .from(messages)
723
+ .where(eq(messages.userId, userId));
724
+ expect(remainingMessages).toHaveLength(0);
725
+ });
726
+ });
727
+
728
+ describe('updateConfig', () => {
729
+ it('should update agent config', async () => {
730
+ // Create test agent
731
+ const agentId = 'test-agent';
732
+ await serverDB.insert(agents).values({
733
+ id: agentId,
734
+ userId,
735
+ model: 'gpt-3.5-turbo',
736
+ title: 'Original Title',
737
+ });
738
+
739
+ // Update config
740
+ await sessionModel.updateConfig(agentId, {
741
+ model: 'gpt-4',
742
+ title: 'Updated Title',
743
+ description: 'New description',
744
+ });
745
+
746
+ // Verify update
747
+ const updatedAgent = await serverDB
748
+ .select()
749
+ .from(agents)
750
+ .where(and(eq(agents.id, agentId), eq(agents.userId, userId)));
751
+
752
+ expect(updatedAgent[0]).toMatchObject({
753
+ model: 'gpt-4',
754
+ title: 'Updated Title',
755
+ description: 'New description',
756
+ });
757
+ });
758
+
759
+ it('should not update config for other users agents', async () => {
760
+ // Create agent for another user
761
+ const agentId = 'other-agent';
762
+ await serverDB.insert(users).values([{ id: 'other-user' }]);
763
+ await serverDB.insert(agents).values({
764
+ id: agentId,
765
+ userId: 'other-user',
766
+ model: 'gpt-3.5-turbo',
767
+ title: 'Original Title',
768
+ });
769
+
770
+ // Try to update other user's agent
771
+ await sessionModel.updateConfig(agentId, {
772
+ model: 'gpt-4',
773
+ title: 'Updated Title',
774
+ });
775
+
776
+ // Verify no changes were made
777
+ const agent = await serverDB.select().from(agents).where(eq(agents.id, agentId));
778
+
779
+ expect(agent[0]).toMatchObject({
780
+ model: 'gpt-3.5-turbo',
781
+ title: 'Original Title',
782
+ });
783
+ });
784
+ });
627
785
  });
@@ -10,14 +10,8 @@ import { SessionGroupModel } from '../sessionGroup';
10
10
 
11
11
  let serverDB = await getTestDBInstance();
12
12
 
13
- vi.mock('@/database/server/core/db', async () => ({
14
- get serverDB() {
15
- return serverDB;
16
- },
17
- }));
18
-
19
13
  const userId = 'session-group-model-test-user-id';
20
- const sessionGroupModel = new SessionGroupModel(userId);
14
+ const sessionGroupModel = new SessionGroupModel(serverDB, userId);
21
15
 
22
16
  beforeEach(async () => {
23
17
  await serverDB.delete(users);
@@ -75,7 +69,7 @@ describe('SessionGroupModel', () => {
75
69
  await sessionGroupModel.create({ name: 'Test Group 1' });
76
70
  await sessionGroupModel.create({ name: 'Test Group 333' });
77
71
 
78
- const anotherSessionGroupModel = new SessionGroupModel('user2');
72
+ const anotherSessionGroupModel = new SessionGroupModel(serverDB, 'user2');
79
73
  await anotherSessionGroupModel.create({ name: 'Test Group 2' });
80
74
 
81
75
  await sessionGroupModel.deleteAll();
@@ -8,15 +8,9 @@ import { CreateTopicParams, TopicModel } from '../topic';
8
8
 
9
9
  let serverDB = await getTestDBInstance();
10
10
 
11
- vi.mock('@/database/server/core/db', async () => ({
12
- get serverDB() {
13
- return serverDB;
14
- },
15
- }));
16
-
17
11
  const userId = 'topic-user-test';
18
12
  const sessionId = 'topic-session';
19
- const topicModel = new TopicModel(userId);
13
+ const topicModel = new TopicModel(serverDB, userId);
20
14
 
21
15
  describe('TopicModel', () => {
22
16
  beforeEach(async () => {