@lobehub/lobehub 2.0.0-next.41 → 2.0.0-next.43
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/package.json +1 -1
- package/packages/database/src/models/__tests__/messages/message.create.test.ts +75 -18
- package/packages/database/src/models/__tests__/messages/message.query.test.ts +223 -6
- package/packages/database/src/models/__tests__/messages/message.stats.test.ts +56 -7
- package/packages/database/src/models/__tests__/messages/message.update.test.ts +45 -4
- package/packages/database/src/models/message.ts +3 -5
- package/packages/utils/src/clientIP.ts +6 -6
- package/packages/utils/src/compressImage.ts +3 -3
- package/packages/utils/src/fetch/fetchSSE.ts +15 -15
- package/packages/utils/src/format.ts +2 -2
- package/packages/utils/src/merge.ts +3 -3
- package/packages/utils/src/parseModels.ts +3 -3
- package/packages/utils/src/sanitizeUTF8.ts +4 -4
- package/packages/utils/src/toolManifest.ts +4 -4
- package/packages/utils/src/trace.test.ts +359 -0
- package/packages/utils/src/uriParser.ts +4 -4
- package/src/features/ChatItem/components/Title.tsx +20 -16
- package/src/features/Conversation/Messages/Assistant/index.tsx +3 -2
- package/src/features/Conversation/Messages/Group/index.tsx +10 -3
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +29 -3
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +8 -2
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -4
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +1 -1
- package/src/store/global/initialState.ts +5 -0
- package/src/store/global/selectors/systemStatus.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.43](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.42...v2.0.0-next.43)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-11-09**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Abnormal animation of tokens.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Abnormal animation of tokens, closes [#10106](https://github.com/lobehub/lobe-chat/issues/10106) ([129df7b](https://github.com/lobehub/lobe-chat/commit/129df7b))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
## [Version 2.0.0-next.42](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.41...v2.0.0-next.42)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2025-11-09**</sup>
|
|
33
|
+
|
|
34
|
+
#### 🐛 Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **misc**: Fix missing messages when finish runtime.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### What's fixed
|
|
44
|
+
|
|
45
|
+
- **misc**: Fix missing messages when finish runtime, closes [#10138](https://github.com/lobehub/lobe-chat/issues/10138) ([b94d477](https://github.com/lobehub/lobe-chat/commit/b94d477))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
## [Version 2.0.0-next.41](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.40...v2.0.0-next.41)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2025-11-09**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"fixes": [
|
|
5
|
+
"Abnormal animation of tokens."
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"date": "2025-11-09",
|
|
9
|
+
"version": "2.0.0-next.43"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"children": {
|
|
13
|
+
"fixes": [
|
|
14
|
+
"Fix missing messages when finish runtime."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"date": "2025-11-09",
|
|
18
|
+
"version": "2.0.0-next.42"
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"children": {
|
|
4
22
|
"improvements": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.43",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
5
5
|
import { uuid } from '@/utils/uuid';
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
chatGroups,
|
|
8
9
|
chunks,
|
|
9
10
|
embeddings,
|
|
10
11
|
files,
|
|
@@ -224,31 +225,53 @@ describe('MessageModel Create Tests', () => {
|
|
|
224
225
|
expect(dbResult[0].id).toBe(customId);
|
|
225
226
|
});
|
|
226
227
|
|
|
227
|
-
it
|
|
228
|
-
// Create test data
|
|
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
|
|
229
230
|
const chunkId1 = uuid();
|
|
230
231
|
const chunkId2 = uuid();
|
|
231
|
-
const
|
|
232
|
+
const firstMessageId = uuid();
|
|
233
|
+
const secondMessageId = uuid();
|
|
232
234
|
|
|
235
|
+
// 1. Create chunks first
|
|
233
236
|
await serverDB.insert(chunks).values([
|
|
234
|
-
{ id: chunkId1, text: 'chunk text 1' },
|
|
235
|
-
{ id: chunkId2, text: 'chunk text 2' },
|
|
237
|
+
{ id: chunkId1, text: 'chunk text 1', userId },
|
|
238
|
+
{ id: chunkId2, text: 'chunk text 2', userId },
|
|
236
239
|
]);
|
|
237
240
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
{ id: chunkId2, similarity: 0.85 },
|
|
245
|
-
],
|
|
246
|
-
ragQueryId,
|
|
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
247
|
sessionId: '1',
|
|
248
248
|
});
|
|
249
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
|
+
|
|
250
273
|
// Verify message created successfully
|
|
251
|
-
expect(result.id).
|
|
274
|
+
expect(result.id).toBe(secondMessageId);
|
|
252
275
|
|
|
253
276
|
// Verify message query chunk associations created successfully
|
|
254
277
|
const queryChunks = await serverDB
|
|
@@ -258,10 +281,10 @@ describe('MessageModel Create Tests', () => {
|
|
|
258
281
|
|
|
259
282
|
expect(queryChunks).toHaveLength(2);
|
|
260
283
|
expect(queryChunks[0].chunkId).toBe(chunkId1);
|
|
261
|
-
expect(queryChunks[0].queryId).toBe(
|
|
262
|
-
expect(queryChunks[0].similarity).toBe('0.
|
|
284
|
+
expect(queryChunks[0].queryId).toBe(messageQuery.id);
|
|
285
|
+
expect(queryChunks[0].similarity).toBe('0.95000');
|
|
263
286
|
expect(queryChunks[1].chunkId).toBe(chunkId2);
|
|
264
|
-
expect(queryChunks[1].similarity).toBe('0.
|
|
287
|
+
expect(queryChunks[1].similarity).toBe('0.85000');
|
|
265
288
|
});
|
|
266
289
|
|
|
267
290
|
it('should create a message with files', async () => {
|
|
@@ -350,6 +373,40 @@ describe('MessageModel Create Tests', () => {
|
|
|
350
373
|
expect(result[0].content).toBe('message 1');
|
|
351
374
|
expect(result[1].content).toBe('message 2');
|
|
352
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
|
+
});
|
|
353
410
|
});
|
|
354
411
|
|
|
355
412
|
describe('createMessageQuery', () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { INBOX_SESSION_ID } from '@lobechat/const';
|
|
1
2
|
import dayjs from 'dayjs';
|
|
2
3
|
import { eq } from 'drizzle-orm';
|
|
3
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
agents,
|
|
9
10
|
chatGroups,
|
|
10
11
|
chunks,
|
|
12
|
+
documents,
|
|
11
13
|
embeddings,
|
|
12
14
|
fileChunks,
|
|
13
15
|
files,
|
|
@@ -389,7 +391,7 @@ describe('MessageModel Query Tests', () => {
|
|
|
389
391
|
expect(result[0].ragRawQuery).toBe('original query');
|
|
390
392
|
});
|
|
391
393
|
|
|
392
|
-
it
|
|
394
|
+
it('should handle multiple message queries for the same message', async () => {
|
|
393
395
|
// Create test data
|
|
394
396
|
const messageId = 'msg-multi-query';
|
|
395
397
|
const queryId1 = uuid();
|
|
@@ -402,7 +404,9 @@ describe('MessageModel Query Tests', () => {
|
|
|
402
404
|
content: 'test message',
|
|
403
405
|
});
|
|
404
406
|
|
|
405
|
-
//
|
|
407
|
+
// 创建两个查询,查询结果应该只包含其中一个
|
|
408
|
+
// Note: 由于 messageQueries 表没有排序字段,返回哪个 query 是不确定的
|
|
409
|
+
// 但应该只返回一个
|
|
406
410
|
await serverDB.insert(messageQueries).values([
|
|
407
411
|
{
|
|
408
412
|
id: queryId1,
|
|
@@ -423,12 +427,13 @@ describe('MessageModel Query Tests', () => {
|
|
|
423
427
|
// Call query method
|
|
424
428
|
const result = await messageModel.query();
|
|
425
429
|
|
|
426
|
-
// Assert result -
|
|
430
|
+
// Assert result - 应该只包含一个查询(具体是哪个取决于数据库实现)
|
|
427
431
|
expect(result).toHaveLength(1);
|
|
428
432
|
expect(result[0].id).toBe(messageId);
|
|
429
|
-
|
|
430
|
-
expect(result[0].
|
|
431
|
-
expect(
|
|
433
|
+
// 验证返回的是两个 query 中的一个
|
|
434
|
+
expect([queryId1, queryId2]).toContain(result[0].ragQueryId);
|
|
435
|
+
expect(['rewritten query 1', 'rewritten query 2']).toContain(result[0].ragQuery);
|
|
436
|
+
expect(['original query 1', 'original query 2']).toContain(result[0].ragRawQuery);
|
|
432
437
|
});
|
|
433
438
|
});
|
|
434
439
|
|
|
@@ -506,6 +511,64 @@ describe('MessageModel Query Tests', () => {
|
|
|
506
511
|
});
|
|
507
512
|
});
|
|
508
513
|
|
|
514
|
+
it('should handle null similarity in chunks and convert to number', async () => {
|
|
515
|
+
await serverDB.transaction(async (trx) => {
|
|
516
|
+
const chunk1Id = uuid();
|
|
517
|
+
const query1Id = uuid();
|
|
518
|
+
|
|
519
|
+
await trx.insert(messages).values({
|
|
520
|
+
id: 'msg1',
|
|
521
|
+
userId,
|
|
522
|
+
role: 'user',
|
|
523
|
+
content: 'test message',
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await trx.insert(files).values({
|
|
527
|
+
id: 'file1',
|
|
528
|
+
userId,
|
|
529
|
+
name: 'test.txt',
|
|
530
|
+
url: 'test-url',
|
|
531
|
+
fileType: 'text/plain',
|
|
532
|
+
size: 100,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
await trx.insert(chunks).values({
|
|
536
|
+
id: chunk1Id,
|
|
537
|
+
text: 'chunk content',
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await trx.insert(fileChunks).values({
|
|
541
|
+
fileId: 'file1',
|
|
542
|
+
userId,
|
|
543
|
+
chunkId: chunk1Id,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await trx.insert(messageQueries).values({
|
|
547
|
+
id: query1Id,
|
|
548
|
+
messageId: 'msg1',
|
|
549
|
+
userId,
|
|
550
|
+
userQuery: 'query',
|
|
551
|
+
rewriteQuery: 'rewritten',
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Insert chunk with null similarity
|
|
555
|
+
await trx.insert(messageQueryChunks).values({
|
|
556
|
+
messageId: 'msg1',
|
|
557
|
+
queryId: query1Id,
|
|
558
|
+
chunkId: chunk1Id,
|
|
559
|
+
similarity: null as any,
|
|
560
|
+
userId,
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const result = await messageModel.query();
|
|
565
|
+
|
|
566
|
+
expect(result).toHaveLength(1);
|
|
567
|
+
expect(result[0].chunksList).toHaveLength(1);
|
|
568
|
+
// null should be converted to undefined
|
|
569
|
+
expect(result[0].chunksList![0].similarity).toBeUndefined();
|
|
570
|
+
});
|
|
571
|
+
|
|
509
572
|
it('should return empty arrays for files and chunks if none exist', async () => {
|
|
510
573
|
await serverDB.insert(messages).values({
|
|
511
574
|
id: 'msg1',
|
|
@@ -890,6 +953,44 @@ describe('MessageModel Query Tests', () => {
|
|
|
890
953
|
expect(result).toHaveLength(1);
|
|
891
954
|
expect(result[0].id).toBe('inbox-msg');
|
|
892
955
|
});
|
|
956
|
+
|
|
957
|
+
it('should query inbox messages when sessionId is INBOX_SESSION_ID', async () => {
|
|
958
|
+
await serverDB.insert(sessions).values([{ id: 'session1', userId }]);
|
|
959
|
+
|
|
960
|
+
await serverDB.insert(messages).values([
|
|
961
|
+
{
|
|
962
|
+
id: 'inbox-msg-1',
|
|
963
|
+
userId,
|
|
964
|
+
sessionId: null,
|
|
965
|
+
role: 'user',
|
|
966
|
+
content: 'inbox message 1',
|
|
967
|
+
createdAt: new Date('2023-01-01'),
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
id: 'inbox-msg-2',
|
|
971
|
+
userId,
|
|
972
|
+
sessionId: null,
|
|
973
|
+
role: 'assistant',
|
|
974
|
+
content: 'inbox message 2',
|
|
975
|
+
createdAt: new Date('2023-01-02'),
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
id: 'session-msg',
|
|
979
|
+
userId,
|
|
980
|
+
sessionId: 'session1',
|
|
981
|
+
role: 'user',
|
|
982
|
+
content: 'session message',
|
|
983
|
+
createdAt: new Date('2023-01-03'),
|
|
984
|
+
},
|
|
985
|
+
]);
|
|
986
|
+
|
|
987
|
+
// Query with INBOX_SESSION_ID should return only inbox messages
|
|
988
|
+
const result = await messageModel.queryBySessionId(INBOX_SESSION_ID);
|
|
989
|
+
|
|
990
|
+
expect(result).toHaveLength(2);
|
|
991
|
+
expect(result[0].id).toBe('inbox-msg-1');
|
|
992
|
+
expect(result[1].id).toBe('inbox-msg-2');
|
|
993
|
+
});
|
|
893
994
|
});
|
|
894
995
|
|
|
895
996
|
describe('queryByKeyWord', () => {
|
|
@@ -928,6 +1029,122 @@ describe('MessageModel Query Tests', () => {
|
|
|
928
1029
|
});
|
|
929
1030
|
});
|
|
930
1031
|
|
|
1032
|
+
describe('query with files edge cases', () => {
|
|
1033
|
+
it('should handle files with empty fileType', async () => {
|
|
1034
|
+
await serverDB.transaction(async (trx) => {
|
|
1035
|
+
await trx.insert(sessions).values({ id: 'session1', userId });
|
|
1036
|
+
|
|
1037
|
+
// Create files with empty string fileType (tests the || '' branch)
|
|
1038
|
+
await trx.insert(files).values([
|
|
1039
|
+
{
|
|
1040
|
+
id: 'file-empty-type',
|
|
1041
|
+
userId,
|
|
1042
|
+
url: 'unknown.bin',
|
|
1043
|
+
name: 'unknown file',
|
|
1044
|
+
fileType: '',
|
|
1045
|
+
size: 1000,
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
id: 'file-image',
|
|
1049
|
+
userId,
|
|
1050
|
+
url: 'image.png',
|
|
1051
|
+
name: 'test image',
|
|
1052
|
+
fileType: 'image/png',
|
|
1053
|
+
size: 2000,
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
id: 'file-video',
|
|
1057
|
+
userId,
|
|
1058
|
+
url: 'video.mp4',
|
|
1059
|
+
name: 'test video',
|
|
1060
|
+
fileType: 'video/mp4',
|
|
1061
|
+
size: 3000,
|
|
1062
|
+
},
|
|
1063
|
+
]);
|
|
1064
|
+
|
|
1065
|
+
const messageId = uuid();
|
|
1066
|
+
await trx.insert(messages).values({
|
|
1067
|
+
id: messageId,
|
|
1068
|
+
userId,
|
|
1069
|
+
role: 'user',
|
|
1070
|
+
content: 'Message with various fileTypes',
|
|
1071
|
+
sessionId: 'session1',
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
await trx.insert(messagesFiles).values([
|
|
1075
|
+
{ messageId, fileId: 'file-empty-type', userId },
|
|
1076
|
+
{ messageId, fileId: 'file-image', userId },
|
|
1077
|
+
{ messageId, fileId: 'file-video', userId },
|
|
1078
|
+
]);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
const result = await messageModel.query({ sessionId: 'session1' });
|
|
1082
|
+
|
|
1083
|
+
expect(result).toHaveLength(1);
|
|
1084
|
+
expect(result[0].fileList).toHaveLength(1); // empty fileType should go to fileList
|
|
1085
|
+
expect(result[0].fileList![0].id).toBe('file-empty-type');
|
|
1086
|
+
expect(result[0].imageList).toHaveLength(1);
|
|
1087
|
+
expect(result[0].imageList![0].id).toBe('file-image');
|
|
1088
|
+
expect(result[0].videoList).toHaveLength(1);
|
|
1089
|
+
expect(result[0].videoList![0].id).toBe('file-video');
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
describe('query with documents', () => {
|
|
1094
|
+
it('should include document content when files have associated documents', async () => {
|
|
1095
|
+
// Create a file with an associated document
|
|
1096
|
+
const fileId = uuid();
|
|
1097
|
+
|
|
1098
|
+
await serverDB.transaction(async (trx) => {
|
|
1099
|
+
await trx.insert(sessions).values({ id: 'session1', userId });
|
|
1100
|
+
|
|
1101
|
+
await trx.insert(files).values({
|
|
1102
|
+
id: fileId,
|
|
1103
|
+
userId,
|
|
1104
|
+
url: 'document.pdf',
|
|
1105
|
+
name: 'test.pdf',
|
|
1106
|
+
fileType: 'application/pdf',
|
|
1107
|
+
size: 5000,
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
await trx.insert(documents).values({
|
|
1111
|
+
fileId,
|
|
1112
|
+
userId,
|
|
1113
|
+
content: 'This is the document content for testing',
|
|
1114
|
+
fileType: 'application/pdf',
|
|
1115
|
+
sourceType: 'file',
|
|
1116
|
+
source: 'document.pdf',
|
|
1117
|
+
totalCharCount: 42,
|
|
1118
|
+
totalLineCount: 1,
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
const messageId = uuid();
|
|
1122
|
+
await trx.insert(messages).values({
|
|
1123
|
+
id: messageId,
|
|
1124
|
+
userId,
|
|
1125
|
+
role: 'user',
|
|
1126
|
+
content: 'Message with document',
|
|
1127
|
+
sessionId: 'session1',
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
await trx.insert(messagesFiles).values({
|
|
1131
|
+
messageId,
|
|
1132
|
+
fileId,
|
|
1133
|
+
userId,
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// Query messages - this should trigger the documents processing code
|
|
1138
|
+
const result = await messageModel.query({ sessionId: 'session1' });
|
|
1139
|
+
|
|
1140
|
+
expect(result).toHaveLength(1);
|
|
1141
|
+
expect(result[0].fileList).toBeDefined();
|
|
1142
|
+
expect(result[0].fileList).toHaveLength(1);
|
|
1143
|
+
expect(result[0].fileList![0].id).toBe(fileId);
|
|
1144
|
+
expect(result[0].fileList![0].content).toBe('This is the document content for testing');
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
|
|
931
1148
|
describe('findMessageQueriesById', () => {
|
|
932
1149
|
it('should return undefined for non-existent message query', async () => {
|
|
933
1150
|
const result = await messageModel.findMessageQueriesById('non-existent-id');
|
|
@@ -223,6 +223,54 @@ describe('MessageModel Statistics Tests', () => {
|
|
|
223
223
|
// Assert result
|
|
224
224
|
expect(result).toEqual(0);
|
|
225
225
|
});
|
|
226
|
+
|
|
227
|
+
it('should count words with startDate filter', async () => {
|
|
228
|
+
await serverDB.insert(messages).values([
|
|
229
|
+
{
|
|
230
|
+
id: '1',
|
|
231
|
+
userId,
|
|
232
|
+
role: 'user',
|
|
233
|
+
content: 'old message',
|
|
234
|
+
createdAt: new Date('2023-01-01'),
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: '2',
|
|
238
|
+
userId,
|
|
239
|
+
role: 'user',
|
|
240
|
+
content: 'new message',
|
|
241
|
+
createdAt: new Date('2023-03-01'),
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
const result = await messageModel.countWords({ startDate: '2023-02-01' });
|
|
246
|
+
|
|
247
|
+
// Only 'new message' should be counted = 11 characters
|
|
248
|
+
expect(result).toEqual(11);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should count words with endDate filter', async () => {
|
|
252
|
+
await serverDB.insert(messages).values([
|
|
253
|
+
{
|
|
254
|
+
id: '1',
|
|
255
|
+
userId,
|
|
256
|
+
role: 'user',
|
|
257
|
+
content: 'old message',
|
|
258
|
+
createdAt: new Date('2023-01-01'),
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: '2',
|
|
262
|
+
userId,
|
|
263
|
+
role: 'user',
|
|
264
|
+
content: 'new message',
|
|
265
|
+
createdAt: new Date('2023-03-01'),
|
|
266
|
+
},
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
const result = await messageModel.countWords({ endDate: '2023-02-01' });
|
|
270
|
+
|
|
271
|
+
// Only 'old message' should be counted = 11 characters
|
|
272
|
+
expect(result).toEqual(11);
|
|
273
|
+
});
|
|
226
274
|
});
|
|
227
275
|
|
|
228
276
|
describe('getHeatmaps', () => {
|
|
@@ -379,10 +427,11 @@ describe('MessageModel Statistics Tests', () => {
|
|
|
379
427
|
vi.useRealTimers();
|
|
380
428
|
});
|
|
381
429
|
|
|
382
|
-
it
|
|
383
|
-
//
|
|
430
|
+
it('should return time count correctly when 19:00 time', async () => {
|
|
431
|
+
// 使用固定日期进行测试,使用本地时间避免时区问题
|
|
384
432
|
vi.useFakeTimers();
|
|
385
|
-
|
|
433
|
+
// Use local time at noon to avoid timezone edge cases
|
|
434
|
+
const fixedDate = new Date('2025-04-02T12:00:00');
|
|
386
435
|
vi.setSystemTime(fixedDate);
|
|
387
436
|
|
|
388
437
|
const today = dayjs(fixedDate);
|
|
@@ -390,28 +439,28 @@ describe('MessageModel Statistics Tests', () => {
|
|
|
390
439
|
const oneDayAgoDate = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
391
440
|
const todayDate = today.format('YYYY-MM-DD');
|
|
392
441
|
|
|
393
|
-
// Create test data
|
|
442
|
+
// Create test data using explicit dates to avoid timezone issues
|
|
394
443
|
await serverDB.insert(messages).values([
|
|
395
444
|
{
|
|
396
445
|
id: '1',
|
|
397
446
|
userId,
|
|
398
447
|
role: 'user',
|
|
399
448
|
content: 'message 1',
|
|
400
|
-
createdAt:
|
|
449
|
+
createdAt: new Date(twoDaysAgoDate + 'T10:00:00'),
|
|
401
450
|
},
|
|
402
451
|
{
|
|
403
452
|
id: '2',
|
|
404
453
|
userId,
|
|
405
454
|
role: 'user',
|
|
406
455
|
content: 'message 2',
|
|
407
|
-
createdAt:
|
|
456
|
+
createdAt: new Date(twoDaysAgoDate + 'T14:00:00'),
|
|
408
457
|
},
|
|
409
458
|
{
|
|
410
459
|
id: '3',
|
|
411
460
|
userId,
|
|
412
461
|
role: 'user',
|
|
413
462
|
content: 'message 3',
|
|
414
|
-
createdAt:
|
|
463
|
+
createdAt: new Date(oneDayAgoDate + 'T10:00:00'),
|
|
415
464
|
},
|
|
416
465
|
]);
|
|
417
466
|
|
|
@@ -367,6 +367,25 @@ describe('MessageModel Update Tests', () => {
|
|
|
367
367
|
|
|
368
368
|
expect(otherResult).toHaveLength(1);
|
|
369
369
|
});
|
|
370
|
+
|
|
371
|
+
it('should handle database errors gracefully', async () => {
|
|
372
|
+
// Create test message
|
|
373
|
+
await serverDB.insert(messages).values({
|
|
374
|
+
id: '1',
|
|
375
|
+
content: 'test message',
|
|
376
|
+
role: 'user',
|
|
377
|
+
userId,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Mock database to throw error by trying to update with invalid sessionId reference
|
|
381
|
+
// This should trigger the catch block in the update method
|
|
382
|
+
const result = await messageModel.update('1', {
|
|
383
|
+
// @ts-expect-error - intentionally passing invalid sessionId to trigger error
|
|
384
|
+
sessionId: 'non-existent-session-that-violates-fk',
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(result.success).toBe(false);
|
|
388
|
+
});
|
|
370
389
|
});
|
|
371
390
|
|
|
372
391
|
describe('updatePluginState', () => {
|
|
@@ -392,6 +411,28 @@ describe('MessageModel Update Tests', () => {
|
|
|
392
411
|
expect(result[0].state).toEqual({ key1: 'value1', key2: 'value2' });
|
|
393
412
|
});
|
|
394
413
|
|
|
414
|
+
it('should handle null state in plugin', async () => {
|
|
415
|
+
// Create test data with null state
|
|
416
|
+
await serverDB.insert(messages).values({ id: '1', content: 'abc', role: 'user', userId });
|
|
417
|
+
await serverDB.insert(messagePlugins).values([
|
|
418
|
+
{
|
|
419
|
+
id: '1',
|
|
420
|
+
toolCallId: 'tool1',
|
|
421
|
+
identifier: 'plugin1',
|
|
422
|
+
state: null,
|
|
423
|
+
userId,
|
|
424
|
+
},
|
|
425
|
+
]);
|
|
426
|
+
|
|
427
|
+
// Call updatePluginState method
|
|
428
|
+
await messageModel.updatePluginState('1', { key1: 'value1' });
|
|
429
|
+
|
|
430
|
+
// Assert result - should merge with empty object when state is null
|
|
431
|
+
const result = await serverDB.select().from(messagePlugins).where(eq(messagePlugins.id, '1'));
|
|
432
|
+
|
|
433
|
+
expect(result[0].state).toEqual({ key1: 'value1' });
|
|
434
|
+
});
|
|
435
|
+
|
|
395
436
|
it('should throw an error if plugin does not exist', async () => {
|
|
396
437
|
// 调用 updatePluginState 方法
|
|
397
438
|
await expect(messageModel.updatePluginState('1', { key: 'value' })).rejects.toThrowError(
|
|
@@ -423,10 +464,10 @@ describe('MessageModel Update Tests', () => {
|
|
|
423
464
|
});
|
|
424
465
|
|
|
425
466
|
it('should throw an error if plugin does not exist', async () => {
|
|
426
|
-
// 调用 updatePluginState
|
|
427
|
-
await expect(
|
|
428
|
-
'
|
|
429
|
-
);
|
|
467
|
+
// 调用 updateMessagePlugin 方法(修复:之前错误地调用了 updatePluginState)
|
|
468
|
+
await expect(
|
|
469
|
+
messageModel.updateMessagePlugin('non-existent-id', { identifier: 'test' }),
|
|
470
|
+
).rejects.toThrowError('Plugin not found');
|
|
430
471
|
});
|
|
431
472
|
});
|
|
432
473
|
|