@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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/chat.json +1 -0
- package/locales/bg-BG/chat.json +1 -0
- package/locales/de-DE/chat.json +1 -0
- package/locales/en-US/chat.json +1 -0
- package/locales/es-ES/chat.json +1 -0
- package/locales/fa-IR/chat.json +1 -0
- package/locales/fr-FR/chat.json +1 -0
- package/locales/it-IT/chat.json +1 -0
- package/locales/ja-JP/chat.json +1 -0
- package/locales/ko-KR/chat.json +1 -0
- package/locales/nl-NL/chat.json +1 -0
- package/locales/pl-PL/chat.json +1 -0
- package/locales/pt-BR/chat.json +1 -0
- package/locales/ru-RU/chat.json +1 -0
- package/locales/tr-TR/chat.json +1 -0
- package/locales/vi-VN/chat.json +1 -0
- package/locales/zh-CN/chat.json +1 -0
- package/locales/zh-TW/chat.json +1 -0
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/messages/message.create.test.ts +549 -0
- package/packages/database/src/models/__tests__/messages/message.delete.test.ts +481 -0
- package/packages/database/src/models/__tests__/messages/message.query.test.ts +1187 -0
- package/packages/database/src/models/__tests__/messages/message.stats.test.ts +633 -0
- package/packages/database/src/models/__tests__/messages/message.update.test.ts +757 -0
- package/packages/database/src/models/message.ts +5 -55
- 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/server/services/message/index.ts +14 -4
- 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/packages/database/src/models/__tests__/message.test.ts +0 -2632
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { uuid } from '@/utils/uuid';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
agents,
|
|
8
|
+
chatGroups,
|
|
9
|
+
chunks,
|
|
10
|
+
embeddings,
|
|
11
|
+
fileChunks,
|
|
12
|
+
files,
|
|
13
|
+
messagePlugins,
|
|
14
|
+
messageQueries,
|
|
15
|
+
messageQueryChunks,
|
|
16
|
+
messageTTS,
|
|
17
|
+
messageTranslates,
|
|
18
|
+
messages,
|
|
19
|
+
messagesFiles,
|
|
20
|
+
sessions,
|
|
21
|
+
topics,
|
|
22
|
+
users,
|
|
23
|
+
} from '../../../schemas';
|
|
24
|
+
import { LobeChatDatabase } from '../../../type';
|
|
25
|
+
import { MessageModel } from '../../message';
|
|
26
|
+
import { getTestDB } from '../_util';
|
|
27
|
+
import { codeEmbedding } from '../fixtures/embedding';
|
|
28
|
+
|
|
29
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
|
30
|
+
|
|
31
|
+
const userId = 'message-update-test';
|
|
32
|
+
const otherUserId = 'message-update-test-other';
|
|
33
|
+
const messageModel = new MessageModel(serverDB, userId);
|
|
34
|
+
const embeddingsId = uuid();
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
// Clear tables before each test case
|
|
38
|
+
await serverDB.transaction(async (trx) => {
|
|
39
|
+
await trx.delete(users).where(eq(users.id, userId));
|
|
40
|
+
await trx.delete(users).where(eq(users.id, otherUserId));
|
|
41
|
+
await trx.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
|
42
|
+
|
|
43
|
+
await trx.insert(sessions).values([
|
|
44
|
+
// { id: 'session1', userId },
|
|
45
|
+
// { id: 'session2', userId },
|
|
46
|
+
{ id: '1', userId },
|
|
47
|
+
]);
|
|
48
|
+
await trx.insert(files).values({
|
|
49
|
+
id: 'f1',
|
|
50
|
+
userId: userId,
|
|
51
|
+
url: 'abc',
|
|
52
|
+
name: 'file-1',
|
|
53
|
+
fileType: 'image/png',
|
|
54
|
+
size: 1000,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await trx.insert(embeddings).values({
|
|
58
|
+
id: embeddingsId,
|
|
59
|
+
embeddings: codeEmbedding,
|
|
60
|
+
userId,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
// Clear tables after each test case
|
|
67
|
+
await serverDB.delete(users).where(eq(users.id, userId));
|
|
68
|
+
await serverDB.delete(users).where(eq(users.id, otherUserId));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('MessageModel Update Tests', () => {
|
|
72
|
+
describe('updateMessage', () => {
|
|
73
|
+
it('should update message content', async () => {
|
|
74
|
+
// Create test data
|
|
75
|
+
await serverDB
|
|
76
|
+
.insert(messages)
|
|
77
|
+
.values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
|
|
78
|
+
|
|
79
|
+
// Call updateMessage method
|
|
80
|
+
await messageModel.update('1', { content: 'updated message' });
|
|
81
|
+
|
|
82
|
+
// Assert result
|
|
83
|
+
const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
|
|
84
|
+
expect(result[0].content).toBe('updated message');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should only update messages belonging to the user', async () => {
|
|
88
|
+
// Create test data
|
|
89
|
+
await serverDB
|
|
90
|
+
.insert(messages)
|
|
91
|
+
.values([{ id: '1', userId: otherUserId, role: 'user', content: 'message 1' }]);
|
|
92
|
+
|
|
93
|
+
// Call updateMessage method
|
|
94
|
+
await messageModel.update('1', { content: 'updated message' });
|
|
95
|
+
|
|
96
|
+
// Assert result
|
|
97
|
+
const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
|
|
98
|
+
expect(result[0].content).toBe('message 1');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should update message tools', async () => {
|
|
102
|
+
// Create test data
|
|
103
|
+
await serverDB.insert(messages).values([
|
|
104
|
+
{
|
|
105
|
+
id: '1',
|
|
106
|
+
userId,
|
|
107
|
+
role: 'user',
|
|
108
|
+
content: 'message 1',
|
|
109
|
+
tools: [
|
|
110
|
+
{
|
|
111
|
+
id: 'call_Z8UU8LedZcoJHFGkfqYecjmT',
|
|
112
|
+
type: 'builtin',
|
|
113
|
+
apiName: 'searchWithSearXNG',
|
|
114
|
+
arguments:
|
|
115
|
+
'{"query":"杭州洪水 2023","searchEngines":["google","bing","baidu","duckduckgo","brave"]}',
|
|
116
|
+
identifier: 'lobe-web-browsing',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// Call updateMessage method
|
|
123
|
+
await messageModel.update('1', {
|
|
124
|
+
tools: [
|
|
125
|
+
{
|
|
126
|
+
id: 'call_Z8UU8LedZcoJHFGkfqYecjmT',
|
|
127
|
+
type: 'builtin',
|
|
128
|
+
apiName: 'searchWithSearXNG',
|
|
129
|
+
arguments: '{"query":"2024 杭州暴雨","searchEngines":["duckduckgo","google","brave"]}',
|
|
130
|
+
identifier: 'lobe-web-browsing',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Assert result
|
|
136
|
+
const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
|
|
137
|
+
expect((result[0].tools as any)[0].arguments).toBe(
|
|
138
|
+
'{"query":"2024 杭州暴雨","searchEngines":["duckduckgo","google","brave"]}',
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('update with imageList', () => {
|
|
143
|
+
it('should update a message and add image files', async () => {
|
|
144
|
+
// Create test data
|
|
145
|
+
await serverDB.insert(messages).values({
|
|
146
|
+
id: 'msg-to-update',
|
|
147
|
+
userId,
|
|
148
|
+
role: 'user',
|
|
149
|
+
content: 'original content',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await serverDB.insert(files).values([
|
|
153
|
+
{
|
|
154
|
+
id: 'img1',
|
|
155
|
+
name: 'image1.jpg',
|
|
156
|
+
fileType: 'image/jpeg',
|
|
157
|
+
size: 100,
|
|
158
|
+
url: 'url1',
|
|
159
|
+
userId,
|
|
160
|
+
},
|
|
161
|
+
{ id: 'img2', name: 'image2.png', fileType: 'image/png', size: 200, url: 'url2', userId },
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
// Call update method
|
|
165
|
+
await messageModel.update('msg-to-update', {
|
|
166
|
+
content: 'updated content',
|
|
167
|
+
imageList: [
|
|
168
|
+
{ id: 'img1', alt: 'image 1', url: 'url1' },
|
|
169
|
+
{ id: 'img2', alt: 'image 2', url: 'url2' },
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Verify message updated successfully
|
|
174
|
+
const updatedMessage = await serverDB
|
|
175
|
+
.select()
|
|
176
|
+
.from(messages)
|
|
177
|
+
.where(eq(messages.id, 'msg-to-update'));
|
|
178
|
+
|
|
179
|
+
expect(updatedMessage[0].content).toBe('updated content');
|
|
180
|
+
|
|
181
|
+
// Verify message file associations created successfully
|
|
182
|
+
const messageFiles = await serverDB
|
|
183
|
+
.select()
|
|
184
|
+
.from(messagesFiles)
|
|
185
|
+
.where(eq(messagesFiles.messageId, 'msg-to-update'));
|
|
186
|
+
|
|
187
|
+
expect(messageFiles).toHaveLength(2);
|
|
188
|
+
expect(messageFiles[0].fileId).toBe('img1');
|
|
189
|
+
expect(messageFiles[1].fileId).toBe('img2');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle empty imageList', async () => {
|
|
193
|
+
// Create test data
|
|
194
|
+
await serverDB.insert(messages).values({
|
|
195
|
+
id: 'msg-no-images',
|
|
196
|
+
userId,
|
|
197
|
+
role: 'user',
|
|
198
|
+
content: 'original content',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Call update method without providing imageList
|
|
202
|
+
await messageModel.update('msg-no-images', {
|
|
203
|
+
content: 'updated content',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Verify message updated successfully
|
|
207
|
+
const updatedMessage = await serverDB
|
|
208
|
+
.select()
|
|
209
|
+
.from(messages)
|
|
210
|
+
.where(eq(messages.id, 'msg-no-images'));
|
|
211
|
+
|
|
212
|
+
expect(updatedMessage[0].content).toBe('updated content');
|
|
213
|
+
|
|
214
|
+
// Verify no message file associations created
|
|
215
|
+
const messageFiles = await serverDB
|
|
216
|
+
.select()
|
|
217
|
+
.from(messagesFiles)
|
|
218
|
+
.where(eq(messagesFiles.messageId, 'msg-no-images'));
|
|
219
|
+
|
|
220
|
+
expect(messageFiles).toHaveLength(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should update multiple fields at once', async () => {
|
|
224
|
+
// Create test data
|
|
225
|
+
await serverDB.insert(messages).values({
|
|
226
|
+
id: 'msg-multi-update',
|
|
227
|
+
userId,
|
|
228
|
+
role: 'user',
|
|
229
|
+
content: 'original content',
|
|
230
|
+
model: 'gpt-3.5',
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Call update method to update multiple fields
|
|
234
|
+
await messageModel.update('msg-multi-update', {
|
|
235
|
+
content: 'updated content',
|
|
236
|
+
role: 'assistant',
|
|
237
|
+
model: 'gpt-4',
|
|
238
|
+
metadata: { tps: 1 },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Verify message updated successfully
|
|
242
|
+
const updatedMessage = await serverDB
|
|
243
|
+
.select()
|
|
244
|
+
.from(messages)
|
|
245
|
+
.where(eq(messages.id, 'msg-multi-update'));
|
|
246
|
+
|
|
247
|
+
expect(updatedMessage[0].content).toBe('updated content');
|
|
248
|
+
expect(updatedMessage[0].role).toBe('assistant');
|
|
249
|
+
expect(updatedMessage[0].model).toBe('gpt-4');
|
|
250
|
+
expect(updatedMessage[0].metadata).toEqual({ tps: 1 });
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('deleteMessage', () => {
|
|
256
|
+
it('should delete a message', async () => {
|
|
257
|
+
// Create test data
|
|
258
|
+
await serverDB
|
|
259
|
+
.insert(messages)
|
|
260
|
+
.values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
|
|
261
|
+
|
|
262
|
+
// 调用 deleteMessage 方法
|
|
263
|
+
await messageModel.deleteMessage('1');
|
|
264
|
+
|
|
265
|
+
// Assert result
|
|
266
|
+
const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
|
|
267
|
+
expect(result).toHaveLength(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should delete a message with tool calls', async () => {
|
|
271
|
+
// Create test data
|
|
272
|
+
await serverDB.transaction(async (trx) => {
|
|
273
|
+
await trx.insert(messages).values([
|
|
274
|
+
{ id: '1', userId, role: 'user', content: 'message 1', tools: [{ id: 'tool1' }] },
|
|
275
|
+
{ id: '2', userId, role: 'tool', content: 'message 1' },
|
|
276
|
+
]);
|
|
277
|
+
await trx
|
|
278
|
+
.insert(messagePlugins)
|
|
279
|
+
.values([{ id: '2', toolCallId: 'tool1', identifier: 'plugin-1', userId }]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// 调用 deleteMessage 方法
|
|
283
|
+
await messageModel.deleteMessage('1');
|
|
284
|
+
|
|
285
|
+
// Assert result
|
|
286
|
+
const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
|
|
287
|
+
expect(result).toHaveLength(0);
|
|
288
|
+
|
|
289
|
+
const result2 = await serverDB
|
|
290
|
+
.select()
|
|
291
|
+
.from(messagePlugins)
|
|
292
|
+
.where(eq(messagePlugins.id, '2'));
|
|
293
|
+
|
|
294
|
+
expect(result2).toHaveLength(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should only delete messages belonging to the user', async () => {
|
|
298
|
+
// Create test data
|
|
299
|
+
await serverDB
|
|
300
|
+
.insert(messages)
|
|
301
|
+
.values([{ id: '1', userId: otherUserId, role: 'user', content: 'message 1' }]);
|
|
302
|
+
|
|
303
|
+
// 调用 deleteMessage 方法
|
|
304
|
+
await messageModel.deleteMessage('1');
|
|
305
|
+
|
|
306
|
+
// Assert result
|
|
307
|
+
const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
|
|
308
|
+
expect(result).toHaveLength(1);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('deleteMessages', () => {
|
|
313
|
+
it('should delete 2 messages', async () => {
|
|
314
|
+
// Create test data
|
|
315
|
+
await serverDB.insert(messages).values([
|
|
316
|
+
{ id: '1', userId, role: 'user', content: 'message 1' },
|
|
317
|
+
{ id: '2', userId, role: 'user', content: 'message 2' },
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
// 调用 deleteMessage 方法
|
|
321
|
+
await messageModel.deleteMessages(['1', '2']);
|
|
322
|
+
|
|
323
|
+
// Assert result
|
|
324
|
+
const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
|
|
325
|
+
expect(result).toHaveLength(0);
|
|
326
|
+
const result2 = await serverDB.select().from(messages).where(eq(messages.id, '2'));
|
|
327
|
+
expect(result2).toHaveLength(0);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should only delete messages belonging to the user', async () => {
|
|
331
|
+
// Create test data
|
|
332
|
+
await serverDB.insert(messages).values([
|
|
333
|
+
{ id: '1', userId: otherUserId, role: 'user', content: 'message 1' },
|
|
334
|
+
{ id: '2', userId: otherUserId, role: 'user', content: 'message 1' },
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
// 调用 deleteMessage 方法
|
|
338
|
+
await messageModel.deleteMessages(['1', '2']);
|
|
339
|
+
|
|
340
|
+
// Assert result
|
|
341
|
+
const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
|
|
342
|
+
expect(result).toHaveLength(1);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('deleteAllMessages', () => {
|
|
347
|
+
it('should delete all messages belonging to the user', async () => {
|
|
348
|
+
// Create test data
|
|
349
|
+
await serverDB.insert(messages).values([
|
|
350
|
+
{ id: '1', userId, role: 'user', content: 'message 1' },
|
|
351
|
+
{ id: '2', userId, role: 'user', content: 'message 2' },
|
|
352
|
+
{ id: '3', userId: otherUserId, role: 'user', content: 'message 3' },
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
// 调用 deleteAllMessages 方法
|
|
356
|
+
await messageModel.deleteAllMessages();
|
|
357
|
+
|
|
358
|
+
// Assert result
|
|
359
|
+
const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
|
|
360
|
+
|
|
361
|
+
expect(result).toHaveLength(0);
|
|
362
|
+
|
|
363
|
+
const otherResult = await serverDB
|
|
364
|
+
.select()
|
|
365
|
+
.from(messages)
|
|
366
|
+
.where(eq(messages.userId, otherUserId));
|
|
367
|
+
|
|
368
|
+
expect(otherResult).toHaveLength(1);
|
|
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
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('updatePluginState', () => {
|
|
392
|
+
it('should update the state field in messagePlugins table', async () => {
|
|
393
|
+
// Create test data
|
|
394
|
+
await serverDB.insert(messages).values({ id: '1', content: 'abc', role: 'user', userId });
|
|
395
|
+
await serverDB.insert(messagePlugins).values([
|
|
396
|
+
{
|
|
397
|
+
id: '1',
|
|
398
|
+
toolCallId: 'tool1',
|
|
399
|
+
identifier: 'plugin1',
|
|
400
|
+
state: { key1: 'value1' },
|
|
401
|
+
userId,
|
|
402
|
+
},
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
// 调用 updatePluginState 方法
|
|
406
|
+
await messageModel.updatePluginState('1', { key2: 'value2' });
|
|
407
|
+
|
|
408
|
+
// Assert result
|
|
409
|
+
const result = await serverDB.select().from(messagePlugins).where(eq(messagePlugins.id, '1'));
|
|
410
|
+
|
|
411
|
+
expect(result[0].state).toEqual({ key1: 'value1', key2: 'value2' });
|
|
412
|
+
});
|
|
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
|
+
|
|
436
|
+
it('should throw an error if plugin does not exist', async () => {
|
|
437
|
+
// 调用 updatePluginState 方法
|
|
438
|
+
await expect(messageModel.updatePluginState('1', { key: 'value' })).rejects.toThrowError(
|
|
439
|
+
'Plugin not found',
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
describe('updateMessagePlugin', () => {
|
|
444
|
+
it('should update the state field in messagePlugins table', async () => {
|
|
445
|
+
// Create test data
|
|
446
|
+
await serverDB.insert(messages).values({ id: '1', content: 'abc', role: 'user', userId });
|
|
447
|
+
await serverDB.insert(messagePlugins).values([
|
|
448
|
+
{
|
|
449
|
+
id: '1',
|
|
450
|
+
toolCallId: 'tool1',
|
|
451
|
+
identifier: 'plugin1',
|
|
452
|
+
state: { key1: 'value1' },
|
|
453
|
+
userId,
|
|
454
|
+
},
|
|
455
|
+
]);
|
|
456
|
+
|
|
457
|
+
// 调用 updatePluginState 方法
|
|
458
|
+
await messageModel.updateMessagePlugin('1', { identifier: 'plugin2' });
|
|
459
|
+
|
|
460
|
+
// Assert result
|
|
461
|
+
const result = await serverDB.select().from(messagePlugins).where(eq(messagePlugins.id, '1'));
|
|
462
|
+
|
|
463
|
+
expect(result[0].identifier).toEqual('plugin2');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should throw an error if plugin does not exist', async () => {
|
|
467
|
+
// 调用 updateMessagePlugin 方法(修复:之前错误地调用了 updatePluginState)
|
|
468
|
+
await expect(
|
|
469
|
+
messageModel.updateMessagePlugin('non-existent-id', { identifier: 'test' }),
|
|
470
|
+
).rejects.toThrowError('Plugin not found');
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('updateMetadata', () => {
|
|
475
|
+
it('should update metadata for an existing message', async () => {
|
|
476
|
+
// Create test data
|
|
477
|
+
await serverDB.insert(messages).values({
|
|
478
|
+
id: 'msg-with-metadata',
|
|
479
|
+
userId,
|
|
480
|
+
role: 'user',
|
|
481
|
+
content: 'test message',
|
|
482
|
+
metadata: { existingKey: 'existingValue' },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// 调用 updateMetadata 方法
|
|
486
|
+
await messageModel.updateMetadata('msg-with-metadata', { newKey: 'newValue' });
|
|
487
|
+
|
|
488
|
+
// Assert result
|
|
489
|
+
const result = await serverDB
|
|
490
|
+
.select()
|
|
491
|
+
.from(messages)
|
|
492
|
+
.where(eq(messages.id, 'msg-with-metadata'));
|
|
493
|
+
|
|
494
|
+
expect(result[0].metadata).toEqual({
|
|
495
|
+
existingKey: 'existingValue',
|
|
496
|
+
newKey: 'newValue',
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should merge new metadata with existing metadata using lodash merge behavior', async () => {
|
|
501
|
+
// Create test data
|
|
502
|
+
await serverDB.insert(messages).values({
|
|
503
|
+
id: 'msg-merge-metadata',
|
|
504
|
+
userId,
|
|
505
|
+
role: 'assistant',
|
|
506
|
+
content: 'test message',
|
|
507
|
+
metadata: {
|
|
508
|
+
level1: {
|
|
509
|
+
level2a: 'original',
|
|
510
|
+
level2b: { level3: 'deep' },
|
|
511
|
+
},
|
|
512
|
+
array: [1, 2, 3],
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// 调用 updateMetadata 方法
|
|
517
|
+
await messageModel.updateMetadata('msg-merge-metadata', {
|
|
518
|
+
level1: {
|
|
519
|
+
level2a: 'updated',
|
|
520
|
+
level2c: 'new',
|
|
521
|
+
},
|
|
522
|
+
newTopLevel: 'value',
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// Assert result - 应该使用 lodash merge 行为
|
|
526
|
+
const result = await serverDB
|
|
527
|
+
.select()
|
|
528
|
+
.from(messages)
|
|
529
|
+
.where(eq(messages.id, 'msg-merge-metadata'));
|
|
530
|
+
|
|
531
|
+
expect(result[0].metadata).toEqual({
|
|
532
|
+
level1: {
|
|
533
|
+
level2a: 'updated',
|
|
534
|
+
level2b: { level3: 'deep' },
|
|
535
|
+
level2c: 'new',
|
|
536
|
+
},
|
|
537
|
+
array: [1, 2, 3],
|
|
538
|
+
newTopLevel: 'value',
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should handle non-existent message IDs', async () => {
|
|
543
|
+
// 调用 updateMetadata 方法,尝试更新不存在的消息
|
|
544
|
+
const result = await messageModel.updateMetadata('non-existent-id', { key: 'value' });
|
|
545
|
+
|
|
546
|
+
// Assert result - 应该返回 undefined
|
|
547
|
+
expect(result).toBeUndefined();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should handle empty metadata updates', async () => {
|
|
551
|
+
// Create test data
|
|
552
|
+
await serverDB.insert(messages).values({
|
|
553
|
+
id: 'msg-empty-metadata',
|
|
554
|
+
userId,
|
|
555
|
+
role: 'user',
|
|
556
|
+
content: 'test message',
|
|
557
|
+
metadata: { originalKey: 'originalValue' },
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// 调用 updateMetadata 方法,传递空对象
|
|
561
|
+
await messageModel.updateMetadata('msg-empty-metadata', {});
|
|
562
|
+
|
|
563
|
+
// Assert result - 原始 metadata 应该保持不变
|
|
564
|
+
const result = await serverDB
|
|
565
|
+
.select()
|
|
566
|
+
.from(messages)
|
|
567
|
+
.where(eq(messages.id, 'msg-empty-metadata'));
|
|
568
|
+
|
|
569
|
+
expect(result[0].metadata).toEqual({ originalKey: 'originalValue' });
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('should handle message with null metadata', async () => {
|
|
573
|
+
// Create test data
|
|
574
|
+
await serverDB.insert(messages).values({
|
|
575
|
+
id: 'msg-null-metadata',
|
|
576
|
+
userId,
|
|
577
|
+
role: 'user',
|
|
578
|
+
content: 'test message',
|
|
579
|
+
metadata: null,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// 调用 updateMetadata 方法
|
|
583
|
+
await messageModel.updateMetadata('msg-null-metadata', { key: 'value' });
|
|
584
|
+
|
|
585
|
+
// Assert result - 应该创建新的 metadata
|
|
586
|
+
const result = await serverDB
|
|
587
|
+
.select()
|
|
588
|
+
.from(messages)
|
|
589
|
+
.where(eq(messages.id, 'msg-null-metadata'));
|
|
590
|
+
|
|
591
|
+
expect(result[0].metadata).toEqual({ key: 'value' });
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should only update messages belonging to the current user', async () => {
|
|
595
|
+
// Create test data - 其他用户的消息
|
|
596
|
+
await serverDB.insert(messages).values({
|
|
597
|
+
id: 'msg-other-user',
|
|
598
|
+
userId: otherUserId,
|
|
599
|
+
role: 'user',
|
|
600
|
+
content: 'test message',
|
|
601
|
+
metadata: { originalKey: 'originalValue' },
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// 调用 updateMetadata 方法
|
|
605
|
+
const result = await messageModel.updateMetadata('msg-other-user', {
|
|
606
|
+
hackedKey: 'hackedValue',
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Assert result - 应该返回 undefined
|
|
610
|
+
expect(result).toBeUndefined();
|
|
611
|
+
|
|
612
|
+
// 验证原始 metadata 未被修改
|
|
613
|
+
const dbResult = await serverDB
|
|
614
|
+
.select()
|
|
615
|
+
.from(messages)
|
|
616
|
+
.where(eq(messages.id, 'msg-other-user'));
|
|
617
|
+
|
|
618
|
+
expect(dbResult[0].metadata).toEqual({ originalKey: 'originalValue' });
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('should handle complex nested metadata updates', async () => {
|
|
622
|
+
// Create test data
|
|
623
|
+
await serverDB.insert(messages).values({
|
|
624
|
+
id: 'msg-complex-metadata',
|
|
625
|
+
userId,
|
|
626
|
+
role: 'assistant',
|
|
627
|
+
content: 'test message',
|
|
628
|
+
metadata: {
|
|
629
|
+
config: {
|
|
630
|
+
settings: {
|
|
631
|
+
enabled: true,
|
|
632
|
+
options: ['a', 'b'],
|
|
633
|
+
},
|
|
634
|
+
version: 1,
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// 调用 updateMetadata 方法
|
|
640
|
+
await messageModel.updateMetadata('msg-complex-metadata', {
|
|
641
|
+
config: {
|
|
642
|
+
settings: {
|
|
643
|
+
enabled: false,
|
|
644
|
+
timeout: 5000,
|
|
645
|
+
},
|
|
646
|
+
newField: 'value',
|
|
647
|
+
},
|
|
648
|
+
stats: { count: 10 },
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Assert result
|
|
652
|
+
const result = await serverDB
|
|
653
|
+
.select()
|
|
654
|
+
.from(messages)
|
|
655
|
+
.where(eq(messages.id, 'msg-complex-metadata'));
|
|
656
|
+
|
|
657
|
+
expect(result[0].metadata).toEqual({
|
|
658
|
+
config: {
|
|
659
|
+
settings: {
|
|
660
|
+
enabled: false,
|
|
661
|
+
options: ['a', 'b'],
|
|
662
|
+
timeout: 5000,
|
|
663
|
+
},
|
|
664
|
+
version: 1,
|
|
665
|
+
newField: 'value',
|
|
666
|
+
},
|
|
667
|
+
stats: { count: 10 },
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
describe('updateTranslate', () => {
|
|
673
|
+
it('should insert a new record if message does not exist in messageTranslates table', async () => {
|
|
674
|
+
// Create test data
|
|
675
|
+
await serverDB
|
|
676
|
+
.insert(messages)
|
|
677
|
+
.values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
|
|
678
|
+
|
|
679
|
+
// 调用 updateTranslate 方法
|
|
680
|
+
await messageModel.updateTranslate('1', {
|
|
681
|
+
content: 'translated message 1',
|
|
682
|
+
from: 'en',
|
|
683
|
+
to: 'zh',
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// Assert result
|
|
687
|
+
const result = await serverDB
|
|
688
|
+
.select()
|
|
689
|
+
.from(messageTranslates)
|
|
690
|
+
.where(eq(messageTranslates.id, '1'));
|
|
691
|
+
|
|
692
|
+
expect(result).toHaveLength(1);
|
|
693
|
+
expect(result[0].content).toBe('translated message 1');
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('should update the corresponding fields if message exists in messageTranslates table', async () => {
|
|
697
|
+
// Create test data
|
|
698
|
+
await serverDB.transaction(async (trx) => {
|
|
699
|
+
await trx
|
|
700
|
+
.insert(messages)
|
|
701
|
+
.values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
|
|
702
|
+
await trx
|
|
703
|
+
.insert(messageTranslates)
|
|
704
|
+
.values([{ id: '1', content: 'translated message 1', from: 'en', to: 'zh', userId }]);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// 调用 updateTranslate 方法
|
|
708
|
+
await messageModel.updateTranslate('1', { content: 'updated translated message 1' });
|
|
709
|
+
|
|
710
|
+
// Assert result
|
|
711
|
+
const result = await serverDB
|
|
712
|
+
.select()
|
|
713
|
+
.from(messageTranslates)
|
|
714
|
+
.where(eq(messageTranslates.id, '1'));
|
|
715
|
+
|
|
716
|
+
expect(result[0].content).toBe('updated translated message 1');
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
describe('updateTTS', () => {
|
|
721
|
+
it('should insert a new record if message does not exist in messageTTS table', async () => {
|
|
722
|
+
// Create test data
|
|
723
|
+
await serverDB
|
|
724
|
+
.insert(messages)
|
|
725
|
+
.values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
|
|
726
|
+
|
|
727
|
+
// 调用 updateTTS 方法
|
|
728
|
+
await messageModel.updateTTS('1', { contentMd5: 'md5', file: 'f1', voice: 'voice1' });
|
|
729
|
+
|
|
730
|
+
// Assert result
|
|
731
|
+
const result = await serverDB.select().from(messageTTS).where(eq(messageTTS.id, '1'));
|
|
732
|
+
|
|
733
|
+
expect(result).toHaveLength(1);
|
|
734
|
+
expect(result[0].voice).toBe('voice1');
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should update the corresponding fields if message exists in messageTTS table', async () => {
|
|
738
|
+
// Create test data
|
|
739
|
+
await serverDB.transaction(async (trx) => {
|
|
740
|
+
await trx
|
|
741
|
+
.insert(messages)
|
|
742
|
+
.values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
|
|
743
|
+
await trx
|
|
744
|
+
.insert(messageTTS)
|
|
745
|
+
.values([{ id: '1', contentMd5: 'md5', fileId: 'f1', voice: 'voice1', userId }]);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// 调用 updateTTS 方法
|
|
749
|
+
await messageModel.updateTTS('1', { voice: 'updated voice1' });
|
|
750
|
+
|
|
751
|
+
// Assert result
|
|
752
|
+
const result = await serverDB.select().from(messageTTS).where(eq(messageTTS.id, '1'));
|
|
753
|
+
|
|
754
|
+
expect(result[0].voice).toBe('updated voice1');
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
});
|