@lobehub/lobehub 2.0.0-next.40 → 2.0.0-next.42

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 (45) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/chat.json +1 -0
  4. package/locales/bg-BG/chat.json +1 -0
  5. package/locales/de-DE/chat.json +1 -0
  6. package/locales/en-US/chat.json +1 -0
  7. package/locales/es-ES/chat.json +1 -0
  8. package/locales/fa-IR/chat.json +1 -0
  9. package/locales/fr-FR/chat.json +1 -0
  10. package/locales/it-IT/chat.json +1 -0
  11. package/locales/ja-JP/chat.json +1 -0
  12. package/locales/ko-KR/chat.json +1 -0
  13. package/locales/nl-NL/chat.json +1 -0
  14. package/locales/pl-PL/chat.json +1 -0
  15. package/locales/pt-BR/chat.json +1 -0
  16. package/locales/ru-RU/chat.json +1 -0
  17. package/locales/tr-TR/chat.json +1 -0
  18. package/locales/vi-VN/chat.json +1 -0
  19. package/locales/zh-CN/chat.json +1 -0
  20. package/locales/zh-TW/chat.json +1 -0
  21. package/package.json +1 -1
  22. package/packages/database/src/models/__tests__/messages/message.create.test.ts +549 -0
  23. package/packages/database/src/models/__tests__/messages/message.delete.test.ts +481 -0
  24. package/packages/database/src/models/__tests__/messages/message.query.test.ts +1187 -0
  25. package/packages/database/src/models/__tests__/messages/message.stats.test.ts +633 -0
  26. package/packages/database/src/models/__tests__/messages/message.update.test.ts +757 -0
  27. package/packages/database/src/models/message.ts +5 -55
  28. package/packages/utils/src/clientIP.ts +6 -6
  29. package/packages/utils/src/compressImage.ts +3 -3
  30. package/packages/utils/src/fetch/fetchSSE.ts +15 -15
  31. package/packages/utils/src/format.ts +2 -2
  32. package/packages/utils/src/merge.ts +3 -3
  33. package/packages/utils/src/parseModels.ts +3 -3
  34. package/packages/utils/src/sanitizeUTF8.ts +4 -4
  35. package/packages/utils/src/toolManifest.ts +4 -4
  36. package/packages/utils/src/trace.test.ts +359 -0
  37. package/packages/utils/src/uriParser.ts +4 -4
  38. package/src/features/ChatItem/components/Title.tsx +20 -16
  39. package/src/features/Conversation/Messages/Assistant/index.tsx +3 -2
  40. package/src/features/Conversation/Messages/Group/index.tsx +10 -3
  41. package/src/server/services/message/index.ts +14 -4
  42. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +8 -2
  43. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -4
  44. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +1 -1
  45. package/packages/database/src/models/__tests__/message.test.ts +0 -2632
@@ -0,0 +1,549 @@
1
+ import { DBMessageItem } from '@lobechat/types';
2
+ import { eq } from 'drizzle-orm';
3
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
+
5
+ import { uuid } from '@/utils/uuid';
6
+
7
+ import {
8
+ chatGroups,
9
+ chunks,
10
+ embeddings,
11
+ files,
12
+ messagePlugins,
13
+ messageQueries,
14
+ messageQueryChunks,
15
+ messages,
16
+ messagesFiles,
17
+ sessions,
18
+ users,
19
+ } from '../../../schemas';
20
+ import { LobeChatDatabase } from '../../../type';
21
+ import { MessageModel } from '../../message';
22
+ import { getTestDB } from '../_util';
23
+ import { codeEmbedding } from '../fixtures/embedding';
24
+
25
+ const serverDB: LobeChatDatabase = await getTestDB();
26
+
27
+ const userId = 'message-create-test';
28
+ const otherUserId = 'message-create-test-other';
29
+ const messageModel = new MessageModel(serverDB, userId);
30
+ const embeddingsId = uuid();
31
+
32
+ beforeEach(async () => {
33
+ // Clear tables before each test case
34
+ await serverDB.transaction(async (trx) => {
35
+ await trx.delete(users).where(eq(users.id, userId));
36
+ await trx.delete(users).where(eq(users.id, otherUserId));
37
+ await trx.insert(users).values([{ id: userId }, { id: otherUserId }]);
38
+
39
+ await trx.insert(sessions).values([
40
+ // { id: 'session1', userId },
41
+ // { id: 'session2', userId },
42
+ { id: '1', userId },
43
+ ]);
44
+ await trx.insert(files).values({
45
+ id: 'f1',
46
+ userId: userId,
47
+ url: 'abc',
48
+ name: 'file-1',
49
+ fileType: 'image/png',
50
+ size: 1000,
51
+ });
52
+
53
+ await trx.insert(embeddings).values({
54
+ id: embeddingsId,
55
+ embeddings: codeEmbedding,
56
+ userId,
57
+ });
58
+ });
59
+ });
60
+
61
+ afterEach(async () => {
62
+ // Clear tables after each test case
63
+ await serverDB.delete(users).where(eq(users.id, userId));
64
+ await serverDB.delete(users).where(eq(users.id, otherUserId));
65
+ });
66
+
67
+ describe('MessageModel Create Tests', () => {
68
+ describe('createMessage', () => {
69
+ it('should create a new message', async () => {
70
+ // Call createMessage method
71
+ await messageModel.create({ role: 'user', content: 'new message', sessionId: '1' });
72
+
73
+ // Assert result
74
+ const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
75
+ expect(result).toHaveLength(1);
76
+ expect(result[0].content).toBe('new message');
77
+ });
78
+
79
+ it('should create a message', async () => {
80
+ const sessionId = 'session1';
81
+ await serverDB.insert(sessions).values([{ id: sessionId, userId }]);
82
+
83
+ const result = await messageModel.create({
84
+ content: 'message 1',
85
+ role: 'user',
86
+ sessionId: 'session1',
87
+ });
88
+
89
+ expect(result.id).toBeDefined();
90
+ expect(result.content).toBe('message 1');
91
+ expect(result.role).toBe('user');
92
+ expect(result.sessionId).toBe('session1');
93
+ expect(result.userId).toBe(userId);
94
+ });
95
+
96
+ it('should generate message ID automatically', async () => {
97
+ // Call createMessage method
98
+ await messageModel.create({
99
+ role: 'user',
100
+ content: 'new message',
101
+ sessionId: '1',
102
+ });
103
+
104
+ // Assert result
105
+ const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
106
+ expect(result[0].id).toBeDefined();
107
+ expect(result[0].id).toHaveLength(18);
108
+ });
109
+
110
+ it('should create a tool message and insert into messagePlugins table', async () => {
111
+ // Call create method
112
+ const result = await messageModel.create({
113
+ content: 'message 1',
114
+ role: 'tool',
115
+ sessionId: '1',
116
+ tool_call_id: 'tool1',
117
+ plugin: {
118
+ apiName: 'api1',
119
+ arguments: 'arg1',
120
+ identifier: 'plugin1',
121
+ type: 'default',
122
+ },
123
+ });
124
+
125
+ // Assert result
126
+ expect(result.id).toBeDefined();
127
+ expect(result.content).toBe('message 1');
128
+ expect(result.role).toBe('tool');
129
+ expect(result.sessionId).toBe('1');
130
+
131
+ const pluginResult = await serverDB
132
+ .select()
133
+ .from(messagePlugins)
134
+ .where(eq(messagePlugins.id, result.id));
135
+ expect(pluginResult).toHaveLength(1);
136
+ expect(pluginResult[0].identifier).toBe('plugin1');
137
+ });
138
+
139
+ it('should create tool message ', async () => {
140
+ // Call create method
141
+ const state = {
142
+ query: 'Composio',
143
+ answers: [],
144
+ results: [
145
+ {
146
+ url: 'https://www.composio.dev/',
147
+ score: 16,
148
+ title: 'Composio - Connect 90+ tools to your AI agents',
149
+ engine: 'bing',
150
+ content:
151
+ 'Faster DevelopmentHigher ReliabilityBetter Integrations. Get Started Now. Our platform lets you ditch the specs and seamlessly integrate any tool you need in less than 5 mins.',
152
+ engines: ['bing', 'qwant', 'brave', 'duckduckgo'],
153
+ category: 'general',
154
+ template: 'default.html',
155
+ positions: [1, 1, 1, 1],
156
+ thumbnail: '',
157
+ parsed_url: ['https', 'www.composio.dev', '/', '', '', ''],
158
+ publishedDate: null,
159
+ },
160
+ {
161
+ url: 'https://www.composio.co/',
162
+ score: 10.75,
163
+ title: 'Composio',
164
+ engine: 'bing',
165
+ content:
166
+ 'Composio was created to help streamline the entire book creation process! Writing. Take time out to write / Make a schedule to write consistently. We have writing software that optimizes your books for printing or ebook format. Figure out what you want to write. Collaborate and write with others. Professional editing is a necessity.',
167
+ engines: ['qwant', 'duckduckgo', 'google', 'bing', 'brave'],
168
+ category: 'general',
169
+ template: 'default.html',
170
+ positions: [5, 2, 1, 5, 4],
171
+ thumbnail: null,
172
+ parsed_url: ['https', 'www.composio.co', '/', '', '', ''],
173
+ publishedDate: null,
174
+ },
175
+ ],
176
+ unresponsive_engines: [],
177
+ };
178
+ const result = await messageModel.create({
179
+ content: '[{}]',
180
+ plugin: {
181
+ apiName: 'searchWithSearXNG',
182
+ arguments: '{\n "query": "Composio"\n}',
183
+ identifier: 'lobe-web-browsing',
184
+ type: 'builtin',
185
+ },
186
+ pluginState: state,
187
+ role: 'tool',
188
+ tool_call_id: 'tool_call_ymxXC2J0',
189
+ sessionId: '1',
190
+ });
191
+
192
+ // Assert result
193
+ expect(result.id).toBeDefined();
194
+ expect(result.content).toBe('[{}]');
195
+ expect(result.role).toBe('tool');
196
+ expect(result.sessionId).toBe('1');
197
+
198
+ const pluginResult = await serverDB
199
+ .select()
200
+ .from(messagePlugins)
201
+ .where(eq(messagePlugins.id, result.id));
202
+ expect(pluginResult).toHaveLength(1);
203
+ expect(pluginResult[0].identifier).toBe('lobe-web-browsing');
204
+ expect(pluginResult[0].state!).toMatchObject(state);
205
+ });
206
+
207
+ describe('create with advanced parameters', () => {
208
+ it('should create a message with custom ID', async () => {
209
+ const customId = 'custom-msg-id';
210
+
211
+ const result = await messageModel.create(
212
+ {
213
+ role: 'user',
214
+ content: 'message with custom ID',
215
+ sessionId: '1',
216
+ },
217
+ customId,
218
+ );
219
+
220
+ expect(result.id).toBe(customId);
221
+
222
+ // Verify database records
223
+ const dbResult = await serverDB.select().from(messages).where(eq(messages.id, customId));
224
+ expect(dbResult).toHaveLength(1);
225
+ expect(dbResult[0].id).toBe(customId);
226
+ });
227
+
228
+ it('should create a message with file chunks and RAG query ID', async () => {
229
+ // Create test data following proper order: message -> query -> message with chunks
230
+ const chunkId1 = uuid();
231
+ const chunkId2 = uuid();
232
+ const firstMessageId = uuid();
233
+ const secondMessageId = uuid();
234
+
235
+ // 1. Create chunks first
236
+ await serverDB.insert(chunks).values([
237
+ { id: chunkId1, text: 'chunk text 1', userId },
238
+ { id: chunkId2, text: 'chunk text 2', userId },
239
+ ]);
240
+
241
+ // 2. Create first message (required for messageQuery FK)
242
+ await serverDB.insert(messages).values({
243
+ id: firstMessageId,
244
+ userId,
245
+ role: 'user',
246
+ content: 'user query',
247
+ sessionId: '1',
248
+ });
249
+
250
+ // 3. Create message query linked to first message
251
+ const messageQuery = await messageModel.createMessageQuery({
252
+ messageId: firstMessageId,
253
+ rewriteQuery: 'test query',
254
+ userQuery: 'original query',
255
+ embeddingsId,
256
+ });
257
+
258
+ // 4. Create second message with file chunks referencing the query
259
+ const result = await messageModel.create(
260
+ {
261
+ role: 'assistant',
262
+ content: 'message with file chunks',
263
+ fileChunks: [
264
+ { id: chunkId1, similarity: 0.95 },
265
+ { id: chunkId2, similarity: 0.85 },
266
+ ],
267
+ ragQueryId: messageQuery.id,
268
+ sessionId: '1',
269
+ },
270
+ secondMessageId,
271
+ );
272
+
273
+ // Verify message created successfully
274
+ expect(result.id).toBe(secondMessageId);
275
+
276
+ // Verify message query chunk associations created successfully
277
+ const queryChunks = await serverDB
278
+ .select()
279
+ .from(messageQueryChunks)
280
+ .where(eq(messageQueryChunks.messageId, result.id));
281
+
282
+ expect(queryChunks).toHaveLength(2);
283
+ expect(queryChunks[0].chunkId).toBe(chunkId1);
284
+ expect(queryChunks[0].queryId).toBe(messageQuery.id);
285
+ expect(queryChunks[0].similarity).toBe('0.95000');
286
+ expect(queryChunks[1].chunkId).toBe(chunkId2);
287
+ expect(queryChunks[1].similarity).toBe('0.85000');
288
+ });
289
+
290
+ it('should create a message with files', async () => {
291
+ // Create test data
292
+ await serverDB.insert(files).values([
293
+ {
294
+ id: 'file1',
295
+ name: 'file1.txt',
296
+ fileType: 'text/plain',
297
+ size: 100,
298
+ url: 'url1',
299
+ userId,
300
+ },
301
+ {
302
+ id: 'file2',
303
+ name: 'file2.jpg',
304
+ fileType: 'image/jpeg',
305
+ size: 200,
306
+ url: 'url2',
307
+ userId,
308
+ },
309
+ ]);
310
+
311
+ // Call create method
312
+ const result = await messageModel.create({
313
+ role: 'user',
314
+ content: 'message with files',
315
+ files: ['file1', 'file2'],
316
+ sessionId: '1',
317
+ });
318
+
319
+ // Verify message created successfully
320
+ expect(result.id).toBeDefined();
321
+
322
+ // Verify message file associations created successfully
323
+ const messageFiles = await serverDB
324
+ .select()
325
+ .from(messagesFiles)
326
+ .where(eq(messagesFiles.messageId, result.id));
327
+
328
+ expect(messageFiles).toHaveLength(2);
329
+ expect(messageFiles[0].fileId).toBe('file1');
330
+ expect(messageFiles[1].fileId).toBe('file2');
331
+ });
332
+
333
+ it('should create a message with custom timestamps', async () => {
334
+ const customCreatedAt = '2022-05-15T10:30:00Z';
335
+ const customUpdatedAt = '2022-05-16T11:45:00Z';
336
+
337
+ const result = await messageModel.create({
338
+ role: 'user',
339
+ content: 'message with custom timestamps',
340
+ createdAt: customCreatedAt as any,
341
+ updatedAt: customUpdatedAt as any,
342
+ sessionId: '1',
343
+ });
344
+
345
+ // Verify database records
346
+ const dbResult = await serverDB.select().from(messages).where(eq(messages.id, result.id));
347
+
348
+ // Date comparison needs to consider timezone and formatting, so use toISOString for comparison
349
+ expect(new Date(dbResult[0].createdAt!).toISOString()).toBe(
350
+ new Date(customCreatedAt).toISOString(),
351
+ );
352
+ expect(new Date(dbResult[0].updatedAt!).toISOString()).toBe(
353
+ new Date(customUpdatedAt).toISOString(),
354
+ );
355
+ });
356
+ });
357
+ });
358
+
359
+ describe('batchCreateMessages', () => {
360
+ it('should batch create messages', async () => {
361
+ // Prepare test data
362
+ const newMessages = [
363
+ { id: '1', role: 'user', content: 'message 1' },
364
+ { id: '2', role: 'assistant', content: 'message 2' },
365
+ ] as DBMessageItem[];
366
+
367
+ // Call batchCreateMessages method
368
+ await messageModel.batchCreate(newMessages);
369
+
370
+ // Assert result
371
+ const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
372
+ expect(result).toHaveLength(2);
373
+ expect(result[0].content).toBe('message 1');
374
+ expect(result[1].content).toBe('message 2');
375
+ });
376
+
377
+ it('should handle messages with and without groupId', async () => {
378
+ await serverDB.insert(sessions).values({ id: 'session1', userId });
379
+ await serverDB.insert(chatGroups).values({ id: 'group1', userId, title: 'Group 1' });
380
+
381
+ // Message without groupId - should keep sessionId
382
+ const msgWithoutGroup = await messageModel.create({
383
+ role: 'user',
384
+ content: 'message without group',
385
+ sessionId: 'session1',
386
+ });
387
+
388
+ // Message with groupId - sessionId should be set to null
389
+ const msgWithGroup = await messageModel.create({
390
+ role: 'user',
391
+ content: 'message with group',
392
+ sessionId: 'session1',
393
+ groupId: 'group1',
394
+ });
395
+
396
+ // Verify from database
397
+ const dbMsgWithoutGroup = await serverDB.query.messages.findFirst({
398
+ where: eq(messages.id, msgWithoutGroup.id),
399
+ });
400
+ const dbMsgWithGroup = await serverDB.query.messages.findFirst({
401
+ where: eq(messages.id, msgWithGroup.id),
402
+ });
403
+
404
+ expect(dbMsgWithoutGroup?.sessionId).toBe('session1');
405
+ expect(dbMsgWithoutGroup?.groupId).toBeNull();
406
+
407
+ expect(dbMsgWithGroup?.sessionId).toBeNull();
408
+ expect(dbMsgWithGroup?.groupId).toBe('group1');
409
+ });
410
+ });
411
+
412
+ describe('createMessageQuery', () => {
413
+ it('should create a new message query', async () => {
414
+ // Create test data
415
+ await serverDB.insert(messages).values({
416
+ id: 'msg1',
417
+ userId,
418
+ role: 'user',
419
+ content: 'test message',
420
+ });
421
+
422
+ // 调用 createMessageQuery 方法
423
+ const result = await messageModel.createMessageQuery({
424
+ messageId: 'msg1',
425
+ userQuery: 'original query',
426
+ rewriteQuery: 'rewritten query',
427
+ embeddingsId,
428
+ });
429
+
430
+ // Assert result
431
+ expect(result).toBeDefined();
432
+ expect(result.id).toBeDefined();
433
+ expect(result.messageId).toBe('msg1');
434
+ expect(result.userQuery).toBe('original query');
435
+ expect(result.rewriteQuery).toBe('rewritten query');
436
+ expect(result.userId).toBe(userId);
437
+
438
+ // 验证数据库中的记录
439
+ const dbResult = await serverDB
440
+ .select()
441
+ .from(messageQueries)
442
+ .where(eq(messageQueries.id, result.id));
443
+
444
+ expect(dbResult).toHaveLength(1);
445
+ expect(dbResult[0].messageId).toBe('msg1');
446
+ expect(dbResult[0].userQuery).toBe('original query');
447
+ expect(dbResult[0].rewriteQuery).toBe('rewritten query');
448
+ });
449
+
450
+ it('should create a message query with embeddings ID', async () => {
451
+ // Create test data
452
+ await serverDB.insert(messages).values({
453
+ id: 'msg2',
454
+ userId,
455
+ role: 'user',
456
+ content: 'test message',
457
+ });
458
+
459
+ // 调用 createMessageQuery 方法
460
+ const result = await messageModel.createMessageQuery({
461
+ messageId: 'msg2',
462
+ userQuery: 'test query',
463
+ rewriteQuery: 'test rewritten query',
464
+ embeddingsId,
465
+ });
466
+
467
+ // Assert result
468
+ expect(result).toBeDefined();
469
+ expect(result.embeddingsId).toBe(embeddingsId);
470
+
471
+ // 验证数据库中的记录
472
+ const dbResult = await serverDB
473
+ .select()
474
+ .from(messageQueries)
475
+ .where(eq(messageQueries.id, result.id));
476
+
477
+ expect(dbResult[0].embeddingsId).toBe(embeddingsId);
478
+ });
479
+
480
+ it('should generate a unique ID for each message query', async () => {
481
+ // Create test data
482
+ await serverDB.insert(messages).values({
483
+ id: 'msg3',
484
+ userId,
485
+ role: 'user',
486
+ content: 'test message',
487
+ });
488
+
489
+ // 连续创建两个消息查询
490
+ const result1 = await messageModel.createMessageQuery({
491
+ messageId: 'msg3',
492
+ userQuery: 'query 1',
493
+ rewriteQuery: 'rewritten query 1',
494
+ embeddingsId,
495
+ });
496
+
497
+ const result2 = await messageModel.createMessageQuery({
498
+ messageId: 'msg3',
499
+ userQuery: 'query 2',
500
+ rewriteQuery: 'rewritten query 2',
501
+ embeddingsId,
502
+ });
503
+
504
+ // Assert result
505
+ expect(result1.id).not.toBe(result2.id);
506
+ });
507
+ });
508
+
509
+ describe('updateMessageRAG', () => {
510
+ it('should insert message query chunks for RAG', async () => {
511
+ // prepare message and query
512
+ const messageId = 'rag-msg-1';
513
+ const queryId = uuid();
514
+ const chunk1 = uuid();
515
+ const chunk2 = uuid();
516
+
517
+ await serverDB.transaction(async (trx) => {
518
+ await trx.insert(messages).values({ id: messageId, role: 'user', userId, content: 'c' });
519
+ await trx.insert(chunks).values([
520
+ { id: chunk1, text: 'a' },
521
+ { id: chunk2, text: 'b' },
522
+ ]);
523
+ await trx
524
+ .insert(messageQueries)
525
+ .values({ id: queryId, messageId, userId, userQuery: 'q', rewriteQuery: 'rq' });
526
+ });
527
+
528
+ await messageModel.updateMessageRAG(messageId, {
529
+ ragQueryId: queryId,
530
+ fileChunks: [
531
+ { id: chunk1, similarity: 0.9 },
532
+ { id: chunk2, similarity: 0.8 },
533
+ ],
534
+ });
535
+
536
+ const rows = await serverDB
537
+ .select()
538
+ .from(messageQueryChunks)
539
+ .where(eq(messageQueryChunks.messageId, messageId));
540
+
541
+ expect(rows).toHaveLength(2);
542
+ const s1 = rows.find((r) => r.chunkId === chunk1)!;
543
+ const s2 = rows.find((r) => r.chunkId === chunk2)!;
544
+ expect(s1.queryId).toBe(queryId);
545
+ expect(s1.similarity).toBe('0.90000');
546
+ expect(s2.similarity).toBe('0.80000');
547
+ });
548
+ });
549
+ });