@lobehub/lobehub 2.0.0-next.29 → 2.0.0-next.30
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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/message.test.ts +0 -129
- package/packages/database/src/models/message.ts +0 -49
- package/packages/utils/src/server/auth.ts +6 -6
- package/packages/utils/src/server/geo.ts +9 -9
- package/packages/utils/src/server/xor.ts +7 -7
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +783 -2
- package/src/server/routers/lambda/message.ts +17 -84
- package/src/server/services/message/__tests__/index.test.ts +348 -0
- package/src/server/services/message/index.ts +159 -0
- package/src/services/message/index.ts +1 -0
|
@@ -11,6 +11,7 @@ import { getServerDB } from '@/database/server';
|
|
|
11
11
|
import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
|
|
12
12
|
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
13
13
|
import { FileService } from '@/server/services/file';
|
|
14
|
+
import { MessageService } from '@/server/services/message';
|
|
14
15
|
|
|
15
16
|
const messageProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
|
|
16
17
|
const { ctx } = opts;
|
|
@@ -19,6 +20,7 @@ const messageProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
|
|
|
19
20
|
ctx: {
|
|
20
21
|
fileService: new FileService(ctx.serverDB, ctx.userId),
|
|
21
22
|
messageModel: new MessageModel(ctx.serverDB, ctx.userId),
|
|
23
|
+
messageService: new MessageService(ctx.serverDB, ctx.userId),
|
|
22
24
|
},
|
|
23
25
|
});
|
|
24
26
|
});
|
|
@@ -53,11 +55,10 @@ export const messageRouter = router({
|
|
|
53
55
|
}),
|
|
54
56
|
|
|
55
57
|
createNewMessage: messageProcedure
|
|
56
|
-
.input(CreateNewMessageParamsSchema)
|
|
58
|
+
.input(CreateNewMessageParamsSchema.extend({ useGroup: z.boolean().optional() }))
|
|
57
59
|
.mutation(async ({ input, ctx }) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
60
|
+
const { useGroup, ...params } = input;
|
|
61
|
+
return ctx.messageService.createNewMessage(params as any, { useGroup });
|
|
61
62
|
}),
|
|
62
63
|
|
|
63
64
|
getHeatmaps: messageProcedure.query(async ({ ctx }) => {
|
|
@@ -109,23 +110,8 @@ export const messageRouter = router({
|
|
|
109
110
|
}),
|
|
110
111
|
)
|
|
111
112
|
.mutation(async ({ input, ctx }) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// If sessionId or topicId is provided, return the full message list
|
|
115
|
-
if (input.sessionId !== undefined || input.topicId !== undefined) {
|
|
116
|
-
const messageList = await ctx.messageModel.query(
|
|
117
|
-
{
|
|
118
|
-
sessionId: input.sessionId,
|
|
119
|
-
topicId: input.topicId,
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
groupAssistantMessages: input.useGroup ?? false,
|
|
123
|
-
postProcessUrl: (path) => ctx.fileService.getFullFileUrl(path),
|
|
124
|
-
},
|
|
125
|
-
);
|
|
126
|
-
return { messages: messageList, success: true };
|
|
127
|
-
}
|
|
128
|
-
return { success: true };
|
|
113
|
+
const { id, ...options } = input;
|
|
114
|
+
return ctx.messageService.removeMessage(id, options);
|
|
129
115
|
}),
|
|
130
116
|
|
|
131
117
|
removeMessageQuery: messageProcedure
|
|
@@ -144,23 +130,8 @@ export const messageRouter = router({
|
|
|
144
130
|
}),
|
|
145
131
|
)
|
|
146
132
|
.mutation(async ({ input, ctx }) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// If sessionId or topicId is provided, return the full message list
|
|
150
|
-
if (input.sessionId !== undefined || input.topicId !== undefined) {
|
|
151
|
-
const messageList = await ctx.messageModel.query(
|
|
152
|
-
{
|
|
153
|
-
sessionId: input.sessionId,
|
|
154
|
-
topicId: input.topicId,
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
groupAssistantMessages: input.useGroup ?? false,
|
|
158
|
-
postProcessUrl: (path) => ctx.fileService.getFullFileUrl(path),
|
|
159
|
-
},
|
|
160
|
-
);
|
|
161
|
-
return { messages: messageList, success: true };
|
|
162
|
-
}
|
|
163
|
-
return { success: true };
|
|
133
|
+
const { ids, ...options } = input;
|
|
134
|
+
return ctx.messageService.removeMessages(ids, options);
|
|
164
135
|
}),
|
|
165
136
|
|
|
166
137
|
removeMessagesByAssistant: messageProcedure
|
|
@@ -207,12 +178,8 @@ export const messageRouter = router({
|
|
|
207
178
|
}),
|
|
208
179
|
)
|
|
209
180
|
.mutation(async ({ input, ctx }) => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
postProcessUrl: (path) => ctx.fileService.getFullFileUrl(path),
|
|
213
|
-
sessionId: input.sessionId,
|
|
214
|
-
topicId: input.topicId,
|
|
215
|
-
});
|
|
181
|
+
const { id, value, ...options } = input;
|
|
182
|
+
return ctx.messageService.updateMessage(id, value as any, options);
|
|
216
183
|
}),
|
|
217
184
|
|
|
218
185
|
updateMessagePlugin: messageProcedure
|
|
@@ -235,23 +202,8 @@ export const messageRouter = router({
|
|
|
235
202
|
}),
|
|
236
203
|
)
|
|
237
204
|
.mutation(async ({ input, ctx }) => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// If sessionId or topicId is provided, return the full message list
|
|
241
|
-
if (input.sessionId !== undefined || input.topicId !== undefined) {
|
|
242
|
-
const messageList = await ctx.messageModel.query(
|
|
243
|
-
{
|
|
244
|
-
sessionId: input.sessionId,
|
|
245
|
-
topicId: input.topicId,
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
groupAssistantMessages: input.useGroup ?? false,
|
|
249
|
-
postProcessUrl: (path) => ctx.fileService.getFullFileUrl(path),
|
|
250
|
-
},
|
|
251
|
-
);
|
|
252
|
-
return { messages: messageList, success: true };
|
|
253
|
-
}
|
|
254
|
-
return { success: true };
|
|
205
|
+
const { id, value, ...options } = input;
|
|
206
|
+
return ctx.messageService.updateMessageRAG(id, value, options);
|
|
255
207
|
}),
|
|
256
208
|
|
|
257
209
|
updateMetadata: messageProcedure
|
|
@@ -276,23 +228,8 @@ export const messageRouter = router({
|
|
|
276
228
|
}),
|
|
277
229
|
)
|
|
278
230
|
.mutation(async ({ input, ctx }) => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
await ctx.messageModel.updateMessagePlugin(input.id, { error: input.value });
|
|
282
|
-
const messageList = await ctx.messageModel.query(
|
|
283
|
-
{
|
|
284
|
-
sessionId: input.sessionId,
|
|
285
|
-
topicId: input.topicId,
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
groupAssistantMessages: input.useGroup ?? false,
|
|
289
|
-
postProcessUrl: (path) => ctx.fileService.getFullFileUrl(path),
|
|
290
|
-
},
|
|
291
|
-
);
|
|
292
|
-
return { messages: messageList, success: true };
|
|
293
|
-
}
|
|
294
|
-
await ctx.messageModel.updateMessagePlugin(input.id, { error: input.value });
|
|
295
|
-
return { success: true };
|
|
231
|
+
const { id, value, ...options } = input;
|
|
232
|
+
return ctx.messageService.updatePluginError(id, value, options);
|
|
296
233
|
}),
|
|
297
234
|
|
|
298
235
|
updatePluginState: messageProcedure
|
|
@@ -306,12 +243,8 @@ export const messageRouter = router({
|
|
|
306
243
|
}),
|
|
307
244
|
)
|
|
308
245
|
.mutation(async ({ input, ctx }) => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
postProcessUrl: (path) => ctx.fileService.getFullFileUrl(path),
|
|
312
|
-
sessionId: input.sessionId,
|
|
313
|
-
topicId: input.topicId,
|
|
314
|
-
});
|
|
246
|
+
const { id, value, ...options } = input;
|
|
247
|
+
return ctx.messageService.updatePluginState(id, value, options);
|
|
315
248
|
}),
|
|
316
249
|
|
|
317
250
|
updateTTS: messageProcedure
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { LobeChatDatabase } from '@lobechat/database';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { MessageModel } from '@/database/models/message';
|
|
5
|
+
import { FileService } from '@/server/services/file';
|
|
6
|
+
|
|
7
|
+
import { MessageService } from '../index';
|
|
8
|
+
|
|
9
|
+
vi.mock('@/database/models/message');
|
|
10
|
+
vi.mock('@/server/services/file');
|
|
11
|
+
|
|
12
|
+
describe('MessageService', () => {
|
|
13
|
+
let messageService: MessageService;
|
|
14
|
+
let mockDB: LobeChatDatabase;
|
|
15
|
+
let mockMessageModel: MessageModel;
|
|
16
|
+
let mockFileService: FileService;
|
|
17
|
+
const userId = 'test-user-id';
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockDB = {} as LobeChatDatabase;
|
|
21
|
+
mockMessageModel = {
|
|
22
|
+
create: vi.fn(),
|
|
23
|
+
deleteMessage: vi.fn(),
|
|
24
|
+
deleteMessages: vi.fn(),
|
|
25
|
+
query: vi.fn(),
|
|
26
|
+
update: vi.fn(),
|
|
27
|
+
updateMessagePlugin: vi.fn(),
|
|
28
|
+
updateMessageRAG: vi.fn(),
|
|
29
|
+
updatePluginState: vi.fn(),
|
|
30
|
+
} as any;
|
|
31
|
+
|
|
32
|
+
mockFileService = {
|
|
33
|
+
getFullFileUrl: vi.fn().mockImplementation((path) => Promise.resolve(`/files${path}`)),
|
|
34
|
+
} as any;
|
|
35
|
+
|
|
36
|
+
// Mock constructors
|
|
37
|
+
vi.mocked(MessageModel).mockImplementation(() => mockMessageModel);
|
|
38
|
+
vi.mocked(FileService).mockImplementation(() => mockFileService);
|
|
39
|
+
|
|
40
|
+
messageService = new MessageService(mockDB, userId);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('removeMessage', () => {
|
|
44
|
+
it('should delete message and return { success: true } when no sessionId/topicId provided', async () => {
|
|
45
|
+
const messageId = 'msg-1';
|
|
46
|
+
|
|
47
|
+
const result = await messageService.removeMessage(messageId);
|
|
48
|
+
|
|
49
|
+
expect(mockMessageModel.deleteMessage).toHaveBeenCalledWith(messageId);
|
|
50
|
+
expect(result).toEqual({ success: true });
|
|
51
|
+
expect(mockMessageModel.query).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should delete message and return message list when sessionId provided', async () => {
|
|
55
|
+
const messageId = 'msg-1';
|
|
56
|
+
const mockMessages = [{ id: 'msg-2', content: 'test' }];
|
|
57
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
58
|
+
|
|
59
|
+
const result = await messageService.removeMessage(messageId, { sessionId: 'session-1' });
|
|
60
|
+
|
|
61
|
+
expect(mockMessageModel.deleteMessage).toHaveBeenCalledWith(messageId);
|
|
62
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
63
|
+
{ groupId: undefined, sessionId: 'session-1', topicId: undefined },
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
groupAssistantMessages: false,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should delete message and return message list when topicId provided', async () => {
|
|
72
|
+
const messageId = 'msg-1';
|
|
73
|
+
const mockMessages = [{ id: 'msg-2', content: 'test' }];
|
|
74
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
75
|
+
|
|
76
|
+
const result = await messageService.removeMessage(messageId, { topicId: 'topic-1' });
|
|
77
|
+
|
|
78
|
+
expect(mockMessageModel.deleteMessage).toHaveBeenCalledWith(messageId);
|
|
79
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
80
|
+
{ groupId: undefined, sessionId: undefined, topicId: 'topic-1' },
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
groupAssistantMessages: false,
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('removeMessages', () => {
|
|
90
|
+
it('should delete messages and return { success: true } when no sessionId/topicId provided', async () => {
|
|
91
|
+
const messageIds = ['msg-1', 'msg-2'];
|
|
92
|
+
|
|
93
|
+
const result = await messageService.removeMessages(messageIds);
|
|
94
|
+
|
|
95
|
+
expect(mockMessageModel.deleteMessages).toHaveBeenCalledWith(messageIds);
|
|
96
|
+
expect(result).toEqual({ success: true });
|
|
97
|
+
expect(mockMessageModel.query).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should delete messages and return message list when sessionId provided', async () => {
|
|
101
|
+
const messageIds = ['msg-1', 'msg-2'];
|
|
102
|
+
const mockMessages = [{ id: 'msg-3', content: 'test' }];
|
|
103
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
104
|
+
|
|
105
|
+
const result = await messageService.removeMessages(messageIds, { sessionId: 'session-1' });
|
|
106
|
+
|
|
107
|
+
expect(mockMessageModel.deleteMessages).toHaveBeenCalledWith(messageIds);
|
|
108
|
+
expect(mockMessageModel.query).toHaveBeenCalled();
|
|
109
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('updateMessageRAG', () => {
|
|
114
|
+
it('should update RAG and return { success: true } when no sessionId/topicId provided', async () => {
|
|
115
|
+
const messageId = 'msg-1';
|
|
116
|
+
const ragValue = { fileChunks: [{ id: 'chunk-1', similarity: 0.95 }] };
|
|
117
|
+
|
|
118
|
+
const result = await messageService.updateMessageRAG(messageId, ragValue);
|
|
119
|
+
|
|
120
|
+
expect(mockMessageModel.updateMessageRAG).toHaveBeenCalledWith(messageId, ragValue);
|
|
121
|
+
expect(result).toEqual({ success: true });
|
|
122
|
+
expect(mockMessageModel.query).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should update RAG and return message list when sessionId provided', async () => {
|
|
126
|
+
const messageId = 'msg-1';
|
|
127
|
+
const ragValue = { fileChunks: [{ id: 'chunk-1', similarity: 0.95 }] };
|
|
128
|
+
const mockMessages = [{ id: 'msg-1', content: 'test' }];
|
|
129
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
130
|
+
|
|
131
|
+
const result = await messageService.updateMessageRAG(messageId, ragValue, {
|
|
132
|
+
sessionId: 'session-1',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(mockMessageModel.updateMessageRAG).toHaveBeenCalledWith(messageId, ragValue);
|
|
136
|
+
expect(mockMessageModel.query).toHaveBeenCalled();
|
|
137
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('updatePluginError', () => {
|
|
142
|
+
it('should update plugin error and return { success: true } when no sessionId/topicId provided', async () => {
|
|
143
|
+
const messageId = 'msg-1';
|
|
144
|
+
const error = { type: 'TestError', message: 'Test error message' };
|
|
145
|
+
|
|
146
|
+
const result = await messageService.updatePluginError(messageId, error);
|
|
147
|
+
|
|
148
|
+
expect(mockMessageModel.updateMessagePlugin).toHaveBeenCalledWith(messageId, { error });
|
|
149
|
+
expect(result).toEqual({ success: true });
|
|
150
|
+
expect(mockMessageModel.query).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should update plugin error and return message list when sessionId provided', async () => {
|
|
154
|
+
const messageId = 'msg-1';
|
|
155
|
+
const error = { type: 'TestError', message: 'Test error message' };
|
|
156
|
+
const mockMessages = [{ id: 'msg-1', content: 'test' }];
|
|
157
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
158
|
+
|
|
159
|
+
const result = await messageService.updatePluginError(messageId, error, {
|
|
160
|
+
sessionId: 'session-1',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(mockMessageModel.updateMessagePlugin).toHaveBeenCalledWith(messageId, { error });
|
|
164
|
+
expect(mockMessageModel.query).toHaveBeenCalled();
|
|
165
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('updatePluginState', () => {
|
|
170
|
+
it('should update plugin state and return { success: true } when no sessionId/topicId provided', async () => {
|
|
171
|
+
const messageId = 'msg-1';
|
|
172
|
+
const state = { key: 'value' };
|
|
173
|
+
|
|
174
|
+
const result = await messageService.updatePluginState(messageId, state, {});
|
|
175
|
+
|
|
176
|
+
expect(mockMessageModel.updatePluginState).toHaveBeenCalledWith(messageId, state);
|
|
177
|
+
expect(result).toEqual({ success: true });
|
|
178
|
+
expect(mockMessageModel.query).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should update plugin state and return message list when sessionId provided', async () => {
|
|
182
|
+
const messageId = 'msg-1';
|
|
183
|
+
const state = { key: 'value' };
|
|
184
|
+
const mockMessages = [{ id: 'msg-1', content: 'test' }];
|
|
185
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
186
|
+
|
|
187
|
+
const result = await messageService.updatePluginState(messageId, state, {
|
|
188
|
+
sessionId: 'session-1',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(mockMessageModel.updatePluginState).toHaveBeenCalledWith(messageId, state);
|
|
192
|
+
expect(mockMessageModel.query).toHaveBeenCalled();
|
|
193
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('updateMessage', () => {
|
|
198
|
+
it('should update message and return { success: true } when no sessionId/topicId provided', async () => {
|
|
199
|
+
const messageId = 'msg-1';
|
|
200
|
+
const value = { content: 'updated content' };
|
|
201
|
+
|
|
202
|
+
const result = await messageService.updateMessage(messageId, value as any, {});
|
|
203
|
+
|
|
204
|
+
expect(mockMessageModel.update).toHaveBeenCalledWith(messageId, value);
|
|
205
|
+
expect(result).toEqual({ success: true });
|
|
206
|
+
expect(mockMessageModel.query).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should update message and return message list when sessionId provided', async () => {
|
|
210
|
+
const messageId = 'msg-1';
|
|
211
|
+
const value = { content: 'updated content' };
|
|
212
|
+
const mockMessages = [{ id: 'msg-1', content: 'updated content' }];
|
|
213
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
214
|
+
|
|
215
|
+
const result = await messageService.updateMessage(messageId, value as any, {
|
|
216
|
+
sessionId: 'session-1',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(mockMessageModel.update).toHaveBeenCalledWith(messageId, value);
|
|
220
|
+
expect(mockMessageModel.query).toHaveBeenCalled();
|
|
221
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('useGroup option', () => {
|
|
226
|
+
it('should pass useGroup option to query', async () => {
|
|
227
|
+
const mockMessages = [{ id: 'msg-1', content: 'test' }];
|
|
228
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
229
|
+
|
|
230
|
+
await messageService.removeMessage('msg-1', {
|
|
231
|
+
sessionId: 'session-1',
|
|
232
|
+
useGroup: true,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
236
|
+
expect.anything(),
|
|
237
|
+
expect.objectContaining({
|
|
238
|
+
groupAssistantMessages: true,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should default useGroup to false', async () => {
|
|
244
|
+
const mockMessages = [{ id: 'msg-1', content: 'test' }];
|
|
245
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
246
|
+
|
|
247
|
+
await messageService.removeMessage('msg-1', { sessionId: 'session-1' });
|
|
248
|
+
|
|
249
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
250
|
+
expect.anything(),
|
|
251
|
+
expect.objectContaining({
|
|
252
|
+
groupAssistantMessages: false,
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('createNewMessage', () => {
|
|
259
|
+
it('should create message and return message list', async () => {
|
|
260
|
+
const params = {
|
|
261
|
+
content: 'Hello',
|
|
262
|
+
role: 'user' as const,
|
|
263
|
+
sessionId: 'session-1',
|
|
264
|
+
};
|
|
265
|
+
const createdMessage = { id: 'msg-1', ...params };
|
|
266
|
+
const mockMessages = [createdMessage, { id: 'msg-2', content: 'Hi' }];
|
|
267
|
+
|
|
268
|
+
vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
|
|
269
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
270
|
+
|
|
271
|
+
const result = await messageService.createNewMessage(params as any);
|
|
272
|
+
|
|
273
|
+
expect(mockMessageModel.create).toHaveBeenCalledWith(params);
|
|
274
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
275
|
+
{
|
|
276
|
+
current: 0,
|
|
277
|
+
groupId: undefined,
|
|
278
|
+
pageSize: 9999,
|
|
279
|
+
sessionId: 'session-1',
|
|
280
|
+
topicId: undefined,
|
|
281
|
+
},
|
|
282
|
+
expect.objectContaining({
|
|
283
|
+
groupAssistantMessages: false,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
expect(result).toEqual({
|
|
287
|
+
id: 'msg-1',
|
|
288
|
+
messages: mockMessages,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should create message with useGroup option', async () => {
|
|
293
|
+
const params = {
|
|
294
|
+
content: 'Hello',
|
|
295
|
+
role: 'assistant' as const,
|
|
296
|
+
sessionId: 'session-1',
|
|
297
|
+
};
|
|
298
|
+
const createdMessage = { id: 'msg-1', ...params };
|
|
299
|
+
const mockMessages = [createdMessage];
|
|
300
|
+
|
|
301
|
+
vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
|
|
302
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
303
|
+
|
|
304
|
+
const result = await messageService.createNewMessage(params as any, { useGroup: true });
|
|
305
|
+
|
|
306
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
307
|
+
expect.anything(),
|
|
308
|
+
expect.objectContaining({
|
|
309
|
+
groupAssistantMessages: true,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
expect(result).toEqual({
|
|
313
|
+
id: 'msg-1',
|
|
314
|
+
messages: mockMessages,
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should create message with topicId and groupId', async () => {
|
|
319
|
+
const params = {
|
|
320
|
+
content: 'Hello',
|
|
321
|
+
groupId: 'group-1',
|
|
322
|
+
role: 'user' as const,
|
|
323
|
+
sessionId: 'session-1',
|
|
324
|
+
topicId: 'topic-1',
|
|
325
|
+
};
|
|
326
|
+
const createdMessage = { id: 'msg-1', ...params };
|
|
327
|
+
const mockMessages = [createdMessage];
|
|
328
|
+
|
|
329
|
+
vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
|
|
330
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
331
|
+
|
|
332
|
+
const result = await messageService.createNewMessage(params as any);
|
|
333
|
+
|
|
334
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
335
|
+
{
|
|
336
|
+
current: 0,
|
|
337
|
+
groupId: 'group-1',
|
|
338
|
+
pageSize: 9999,
|
|
339
|
+
sessionId: 'session-1',
|
|
340
|
+
topicId: 'topic-1',
|
|
341
|
+
},
|
|
342
|
+
expect.anything(),
|
|
343
|
+
);
|
|
344
|
+
expect(result.id).toBe('msg-1');
|
|
345
|
+
expect(result.messages).toEqual(mockMessages);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { LobeChatDatabase } from '@lobechat/database';
|
|
2
|
+
import { CreateMessageParams, UpdateMessageParams } from '@lobechat/types';
|
|
3
|
+
|
|
4
|
+
import { MessageModel } from '@/database/models/message';
|
|
5
|
+
|
|
6
|
+
import { FileService } from '../file';
|
|
7
|
+
|
|
8
|
+
interface QueryOptions {
|
|
9
|
+
groupId?: string | null;
|
|
10
|
+
sessionId?: string | null;
|
|
11
|
+
topicId?: string | null;
|
|
12
|
+
useGroup?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CreateMessageResult {
|
|
16
|
+
id: string;
|
|
17
|
+
messages: any[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Message Service
|
|
22
|
+
*
|
|
23
|
+
* Encapsulates repeated "mutation + conditional query" logic.
|
|
24
|
+
* After performing update/delete operations, conditionally returns message list based on sessionId/topicId.
|
|
25
|
+
*/
|
|
26
|
+
export class MessageService {
|
|
27
|
+
private messageModel: MessageModel;
|
|
28
|
+
private fileService: FileService;
|
|
29
|
+
|
|
30
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
|
31
|
+
this.messageModel = new MessageModel(db, userId);
|
|
32
|
+
this.fileService = new FileService(db, userId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Unified URL processing function
|
|
37
|
+
*/
|
|
38
|
+
private get postProcessUrl() {
|
|
39
|
+
return (path: string | null) => this.fileService.getFullFileUrl(path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Unified query options
|
|
44
|
+
*/
|
|
45
|
+
private getQueryOptions(options: QueryOptions) {
|
|
46
|
+
return {
|
|
47
|
+
groupAssistantMessages: options.useGroup ?? false,
|
|
48
|
+
postProcessUrl: this.postProcessUrl,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Query messages and return response with success status (used after mutations)
|
|
54
|
+
*/
|
|
55
|
+
private async queryWithSuccess(options?: QueryOptions) {
|
|
56
|
+
if (!options || (options.sessionId === undefined && options.topicId === undefined)) {
|
|
57
|
+
return { success: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { sessionId, topicId, groupId } = options;
|
|
61
|
+
|
|
62
|
+
const messages = await this.messageModel.query(
|
|
63
|
+
{ groupId, sessionId, topicId },
|
|
64
|
+
this.getQueryOptions(options),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return { messages, success: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remove messages with optional message list return
|
|
72
|
+
* Pattern: delete + conditional query
|
|
73
|
+
*/
|
|
74
|
+
async removeMessages(ids: string[], options?: QueryOptions) {
|
|
75
|
+
await this.messageModel.deleteMessages(ids);
|
|
76
|
+
return this.queryWithSuccess(options);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Remove single message with optional message list return
|
|
81
|
+
* Pattern: delete + conditional query
|
|
82
|
+
*/
|
|
83
|
+
async removeMessage(id: string, options?: QueryOptions) {
|
|
84
|
+
await this.messageModel.deleteMessage(id);
|
|
85
|
+
return this.queryWithSuccess(options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update message RAG with optional message list return
|
|
90
|
+
* Pattern: update + conditional query
|
|
91
|
+
*/
|
|
92
|
+
async updateMessageRAG(id: string, value: any, options?: QueryOptions) {
|
|
93
|
+
await this.messageModel.updateMessageRAG(id, value);
|
|
94
|
+
return this.queryWithSuccess(options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Update plugin error with optional message list return
|
|
99
|
+
* Pattern: update + conditional query
|
|
100
|
+
*/
|
|
101
|
+
async updatePluginError(id: string, value: any, options?: QueryOptions) {
|
|
102
|
+
await this.messageModel.updateMessagePlugin(id, { error: value });
|
|
103
|
+
return this.queryWithSuccess(options);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update plugin state and return message list
|
|
108
|
+
* Pattern: update + conditional query
|
|
109
|
+
*/
|
|
110
|
+
async updatePluginState(id: string, value: any, options: QueryOptions): Promise<any> {
|
|
111
|
+
await this.messageModel.updatePluginState(id, value);
|
|
112
|
+
return this.queryWithSuccess(options);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Update message and return message list
|
|
117
|
+
* Pattern: update + conditional query
|
|
118
|
+
*/
|
|
119
|
+
async updateMessage(id: string, value: UpdateMessageParams, options: QueryOptions): Promise<any> {
|
|
120
|
+
await this.messageModel.update(id, value as any);
|
|
121
|
+
return this.queryWithSuccess(options);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a new message and return the complete message list
|
|
126
|
+
* Pattern: create + query
|
|
127
|
+
*
|
|
128
|
+
* This method combines message creation and querying into a single operation,
|
|
129
|
+
* reducing the need for separate refresh calls and improving performance.
|
|
130
|
+
*/
|
|
131
|
+
async createNewMessage(
|
|
132
|
+
params: CreateMessageParams,
|
|
133
|
+
options?: QueryOptions,
|
|
134
|
+
): Promise<CreateMessageResult> {
|
|
135
|
+
// 1. Create the message
|
|
136
|
+
const item = await this.messageModel.create(params);
|
|
137
|
+
|
|
138
|
+
// 2. Query all messages for this session/topic
|
|
139
|
+
const messages = await this.messageModel.query(
|
|
140
|
+
{
|
|
141
|
+
current: 0,
|
|
142
|
+
groupId: params.groupId,
|
|
143
|
+
pageSize: 9999,
|
|
144
|
+
sessionId: params.sessionId,
|
|
145
|
+
topicId: params.topicId,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
groupAssistantMessages: options?.useGroup ?? false,
|
|
149
|
+
postProcessUrl: this.postProcessUrl,
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// 3. Return the result
|
|
154
|
+
return {
|
|
155
|
+
id: item.id,
|
|
156
|
+
messages,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|