@lobehub/lobehub 2.0.0-next.35 → 2.0.0-next.37
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/next.config.ts +5 -6
- package/package.json +2 -2
- package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
- package/packages/agent-runtime/src/core/runtime.ts +63 -18
- package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
- package/packages/agent-runtime/src/types/index.ts +1 -0
- package/packages/agent-runtime/src/types/instruction.ts +10 -3
- package/packages/const/src/user.ts +0 -1
- package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
- package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
- package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
- package/packages/conversation-flow/src/index.ts +1 -1
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
- package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
- package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
- package/packages/database/src/models/__tests__/apiKey.test.ts +444 -0
- package/packages/database/src/models/message.ts +18 -19
- package/packages/types/src/aiChat.ts +2 -0
- package/packages/types/src/importer.ts +2 -2
- package/packages/types/src/message/ui/chat.ts +17 -1
- package/packages/types/src/message/ui/extra.ts +2 -2
- package/packages/types/src/message/ui/params.ts +2 -2
- package/packages/types/src/user/preference.ts +0 -4
- package/packages/utils/src/tokenizer/index.ts +3 -11
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
- package/src/app/[variants]/(main)/labs/page.tsx +0 -9
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
- package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
- package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
- package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
- package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
- package/src/features/Conversation/Error/index.tsx +0 -5
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
- package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
- package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
- package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
- package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
- package/src/features/Conversation/Messages/Default.tsx +1 -0
- package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
- package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
- package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
- package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
- package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
- package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
- package/src/features/Conversation/Messages/Group/index.tsx +2 -1
- package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
- package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
- package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
- package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
- package/src/features/Conversation/Messages/User/index.tsx +43 -44
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +3 -3
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
- package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
- package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
- package/src/hooks/useHotkeys/chatScope.ts +15 -7
- package/src/libs/trpc/client/lambda.ts +4 -3
- package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
- package/src/server/routers/lambda/aiChat.ts +3 -2
- package/src/server/routers/lambda/message.ts +8 -16
- package/src/server/services/message/__tests__/index.test.ts +29 -39
- package/src/server/services/message/index.ts +41 -36
- package/src/services/electron/desktopNotification.ts +6 -6
- package/src/services/electron/file.ts +6 -6
- package/src/services/file/ClientS3/index.ts +8 -8
- package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
- package/src/services/message/index.ts +21 -15
- package/src/services/upload.ts +11 -11
- package/src/services/utils/abortableRequest.test.ts +161 -0
- package/src/services/utils/abortableRequest.ts +67 -0
- package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
- package/src/store/chat/agents/createAgentExecutors.ts +395 -0
- package/src/store/chat/helpers.test.ts +0 -99
- package/src/store/chat/helpers.ts +0 -11
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
- package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
- package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
- package/src/store/chat/slices/message/action.test.ts +79 -68
- package/src/store/chat/slices/message/actions/index.ts +39 -0
- package/src/store/chat/slices/message/actions/internals.ts +77 -0
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
- package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
- package/src/store/chat/slices/message/actions/query.ts +120 -0
- package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
- package/src/store/chat/slices/message/initialState.ts +13 -0
- package/src/store/chat/slices/message/reducer.test.ts +48 -370
- package/src/store/chat/slices/message/reducer.ts +17 -81
- package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
- package/src/store/chat/slices/message/selectors/chat.ts +78 -242
- package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
- package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
- package/src/store/chat/slices/plugin/action.test.ts +62 -64
- package/src/store/chat/slices/plugin/action.ts +34 -28
- package/src/store/chat/slices/thread/action.test.ts +28 -31
- package/src/store/chat/slices/thread/action.ts +13 -10
- package/src/store/chat/slices/thread/selectors/index.ts +8 -6
- package/src/store/chat/slices/topic/reducer.ts +11 -3
- package/src/store/chat/store.ts +1 -1
- package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
- package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
- package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
- package/packages/database/src/utils/groupMessages.ts +0 -361
- package/packages/utils/src/tokenizer/client.ts +0 -35
- package/packages/utils/src/tokenizer/estimated.ts +0 -4
- package/packages/utils/src/tokenizer/server.ts +0 -11
- package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
- package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
- package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
- package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
- package/src/store/chat/slices/message/action.ts +0 -629
|
@@ -55,10 +55,9 @@ export const messageRouter = router({
|
|
|
55
55
|
}),
|
|
56
56
|
|
|
57
57
|
createMessage: messageProcedure
|
|
58
|
-
.input(CreateNewMessageParamsSchema
|
|
58
|
+
.input(CreateNewMessageParamsSchema)
|
|
59
59
|
.mutation(async ({ input, ctx }) => {
|
|
60
|
-
|
|
61
|
-
return ctx.messageService.createMessage(params as any, { useGroup });
|
|
60
|
+
return ctx.messageService.createMessage(input as any);
|
|
62
61
|
}),
|
|
63
62
|
|
|
64
63
|
getHeatmaps: messageProcedure.query(async ({ ctx }) => {
|
|
@@ -74,20 +73,17 @@ export const messageRouter = router({
|
|
|
74
73
|
pageSize: z.number().optional(),
|
|
75
74
|
sessionId: z.string().nullable().optional(),
|
|
76
75
|
topicId: z.string().nullable().optional(),
|
|
77
|
-
useGroup: z.boolean().optional(),
|
|
78
76
|
}),
|
|
79
77
|
)
|
|
80
78
|
.query(async ({ input, ctx }) => {
|
|
81
79
|
if (!ctx.userId) return [];
|
|
82
80
|
const serverDB = await getServerDB();
|
|
83
81
|
|
|
84
|
-
const { useGroup, ...queryParams } = input;
|
|
85
|
-
|
|
86
82
|
const messageModel = new MessageModel(serverDB, ctx.userId);
|
|
87
83
|
const fileService = new FileService(serverDB, ctx.userId);
|
|
88
84
|
|
|
89
|
-
return messageModel.query(
|
|
90
|
-
groupAssistantMessages:
|
|
85
|
+
return messageModel.query(input, {
|
|
86
|
+
groupAssistantMessages: false,
|
|
91
87
|
postProcessUrl: (path) => fileService.getFullFileUrl(path),
|
|
92
88
|
});
|
|
93
89
|
}),
|
|
@@ -106,7 +102,6 @@ export const messageRouter = router({
|
|
|
106
102
|
id: z.string(),
|
|
107
103
|
sessionId: z.string().nullable().optional(),
|
|
108
104
|
topicId: z.string().nullable().optional(),
|
|
109
|
-
useGroup: z.boolean().optional(),
|
|
110
105
|
}),
|
|
111
106
|
)
|
|
112
107
|
.mutation(async ({ input, ctx }) => {
|
|
@@ -126,7 +121,6 @@ export const messageRouter = router({
|
|
|
126
121
|
ids: z.array(z.string()),
|
|
127
122
|
sessionId: z.string().nullable().optional(),
|
|
128
123
|
topicId: z.string().nullable().optional(),
|
|
129
|
-
useGroup: z.boolean().optional(),
|
|
130
124
|
}),
|
|
131
125
|
)
|
|
132
126
|
.mutation(async ({ input, ctx }) => {
|
|
@@ -173,7 +167,6 @@ export const messageRouter = router({
|
|
|
173
167
|
id: z.string(),
|
|
174
168
|
sessionId: z.string().nullable().optional(),
|
|
175
169
|
topicId: z.string().nullable().optional(),
|
|
176
|
-
useGroup: z.boolean().optional(),
|
|
177
170
|
value: UpdateMessageParamsSchema,
|
|
178
171
|
}),
|
|
179
172
|
)
|
|
@@ -198,7 +191,6 @@ export const messageRouter = router({
|
|
|
198
191
|
UpdateMessageRAGParamsSchema.extend({
|
|
199
192
|
sessionId: z.string().nullable().optional(),
|
|
200
193
|
topicId: z.string().nullable().optional(),
|
|
201
|
-
useGroup: z.boolean().optional(),
|
|
202
194
|
}),
|
|
203
195
|
)
|
|
204
196
|
.mutation(async ({ input, ctx }) => {
|
|
@@ -210,11 +202,14 @@ export const messageRouter = router({
|
|
|
210
202
|
.input(
|
|
211
203
|
z.object({
|
|
212
204
|
id: z.string(),
|
|
205
|
+
sessionId: z.string().nullable().optional(),
|
|
206
|
+
topicId: z.string().nullable().optional(),
|
|
213
207
|
value: z.object({}).passthrough(),
|
|
214
208
|
}),
|
|
215
209
|
)
|
|
216
210
|
.mutation(async ({ input, ctx }) => {
|
|
217
|
-
|
|
211
|
+
const { id, value, ...options } = input;
|
|
212
|
+
return ctx.messageService.updateMetadata(id, value, options);
|
|
218
213
|
}),
|
|
219
214
|
|
|
220
215
|
updatePluginError: messageProcedure
|
|
@@ -223,7 +218,6 @@ export const messageRouter = router({
|
|
|
223
218
|
id: z.string(),
|
|
224
219
|
sessionId: z.string().nullable().optional(),
|
|
225
220
|
topicId: z.string().nullable().optional(),
|
|
226
|
-
useGroup: z.boolean().optional(),
|
|
227
221
|
value: z.object({}).passthrough().nullable(),
|
|
228
222
|
}),
|
|
229
223
|
)
|
|
@@ -238,7 +232,6 @@ export const messageRouter = router({
|
|
|
238
232
|
id: z.string(),
|
|
239
233
|
sessionId: z.string().nullable().optional(),
|
|
240
234
|
topicId: z.string().nullable().optional(),
|
|
241
|
-
useGroup: z.boolean().optional(),
|
|
242
235
|
value: z.object({}).passthrough(),
|
|
243
236
|
}),
|
|
244
237
|
)
|
|
@@ -267,7 +260,6 @@ export const messageRouter = router({
|
|
|
267
260
|
|
|
268
261
|
return ctx.messageModel.updateTTS(input.id, input.value);
|
|
269
262
|
}),
|
|
270
|
-
|
|
271
263
|
updateTranslate: messageProcedure
|
|
272
264
|
.input(
|
|
273
265
|
z.object({
|
|
@@ -26,6 +26,7 @@ describe('MessageService', () => {
|
|
|
26
26
|
update: vi.fn(),
|
|
27
27
|
updateMessagePlugin: vi.fn(),
|
|
28
28
|
updateMessageRAG: vi.fn(),
|
|
29
|
+
updateMetadata: vi.fn(),
|
|
29
30
|
updatePluginState: vi.fn(),
|
|
30
31
|
} as any;
|
|
31
32
|
|
|
@@ -222,36 +223,51 @@ describe('MessageService', () => {
|
|
|
222
223
|
});
|
|
223
224
|
});
|
|
224
225
|
|
|
225
|
-
describe('
|
|
226
|
-
it('should
|
|
226
|
+
describe('updateMetadata', () => {
|
|
227
|
+
it('should update metadata and return { success: true } when no sessionId/topicId provided', async () => {
|
|
228
|
+
const messageId = 'msg-1';
|
|
229
|
+
const metadata = { someKey: 'someValue', count: 42 };
|
|
230
|
+
|
|
231
|
+
const result = await messageService.updateMetadata(messageId, metadata);
|
|
232
|
+
|
|
233
|
+
expect(mockMessageModel.updateMetadata).toHaveBeenCalledWith(messageId, metadata);
|
|
234
|
+
expect(result).toEqual({ success: true });
|
|
235
|
+
expect(mockMessageModel.query).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should update metadata and return message list when sessionId provided', async () => {
|
|
239
|
+
const messageId = 'msg-1';
|
|
240
|
+
const metadata = { someKey: 'someValue', count: 42 };
|
|
227
241
|
const mockMessages = [{ id: 'msg-1', content: 'test' }];
|
|
228
242
|
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
229
243
|
|
|
230
|
-
await messageService.
|
|
244
|
+
const result = await messageService.updateMetadata(messageId, metadata, {
|
|
231
245
|
sessionId: 'session-1',
|
|
232
|
-
useGroup: true,
|
|
233
246
|
});
|
|
234
247
|
|
|
235
|
-
expect(mockMessageModel.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
groupAssistantMessages: true,
|
|
239
|
-
}),
|
|
240
|
-
);
|
|
248
|
+
expect(mockMessageModel.updateMetadata).toHaveBeenCalledWith(messageId, metadata);
|
|
249
|
+
expect(mockMessageModel.query).toHaveBeenCalled();
|
|
250
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
241
251
|
});
|
|
242
252
|
|
|
243
|
-
it('should
|
|
253
|
+
it('should update metadata and return message list when topicId provided', async () => {
|
|
254
|
+
const messageId = 'msg-1';
|
|
255
|
+
const metadata = { key: 'value' };
|
|
244
256
|
const mockMessages = [{ id: 'msg-1', content: 'test' }];
|
|
245
257
|
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
246
258
|
|
|
247
|
-
await messageService.
|
|
259
|
+
const result = await messageService.updateMetadata(messageId, metadata, {
|
|
260
|
+
topicId: 'topic-1',
|
|
261
|
+
});
|
|
248
262
|
|
|
263
|
+
expect(mockMessageModel.updateMetadata).toHaveBeenCalledWith(messageId, metadata);
|
|
249
264
|
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
250
|
-
|
|
265
|
+
{ groupId: undefined, sessionId: undefined, topicId: 'topic-1' },
|
|
251
266
|
expect.objectContaining({
|
|
252
267
|
groupAssistantMessages: false,
|
|
253
268
|
}),
|
|
254
269
|
);
|
|
270
|
+
expect(result).toEqual({ messages: mockMessages, success: true });
|
|
255
271
|
});
|
|
256
272
|
});
|
|
257
273
|
|
|
@@ -289,32 +305,6 @@ describe('MessageService', () => {
|
|
|
289
305
|
});
|
|
290
306
|
});
|
|
291
307
|
|
|
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.createMessage(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
308
|
it('should create message with topicId and groupId', async () => {
|
|
319
309
|
const params = {
|
|
320
310
|
content: 'Hello',
|
|
@@ -9,7 +9,6 @@ interface QueryOptions {
|
|
|
9
9
|
groupId?: string | null;
|
|
10
10
|
sessionId?: string | null;
|
|
11
11
|
topicId?: string | null;
|
|
12
|
-
useGroup?: boolean;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
interface CreateMessageResult {
|
|
@@ -42,9 +41,9 @@ export class MessageService {
|
|
|
42
41
|
/**
|
|
43
42
|
* Unified query options
|
|
44
43
|
*/
|
|
45
|
-
private getQueryOptions(
|
|
44
|
+
private getQueryOptions() {
|
|
46
45
|
return {
|
|
47
|
-
groupAssistantMessages:
|
|
46
|
+
groupAssistantMessages: false,
|
|
48
47
|
postProcessUrl: this.postProcessUrl,
|
|
49
48
|
};
|
|
50
49
|
}
|
|
@@ -61,12 +60,45 @@ export class MessageService {
|
|
|
61
60
|
|
|
62
61
|
const messages = await this.messageModel.query(
|
|
63
62
|
{ groupId, sessionId, topicId },
|
|
64
|
-
this.getQueryOptions(
|
|
63
|
+
this.getQueryOptions(),
|
|
65
64
|
);
|
|
66
65
|
|
|
67
66
|
return { messages, success: true };
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Create a new message and return the complete message list
|
|
71
|
+
* Pattern: create + query
|
|
72
|
+
*
|
|
73
|
+
* This method combines message creation and querying into a single operation,
|
|
74
|
+
* reducing the need for separate refresh calls and improving performance.
|
|
75
|
+
*/
|
|
76
|
+
async createMessage(params: CreateMessageParams): Promise<CreateMessageResult> {
|
|
77
|
+
// 1. Create the message
|
|
78
|
+
const item = await this.messageModel.create(params);
|
|
79
|
+
|
|
80
|
+
// 2. Query all messages for this session/topic
|
|
81
|
+
const messages = await this.messageModel.query(
|
|
82
|
+
{
|
|
83
|
+
current: 0,
|
|
84
|
+
groupId: params.groupId,
|
|
85
|
+
pageSize: 9999,
|
|
86
|
+
sessionId: params.sessionId,
|
|
87
|
+
topicId: params.topicId,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
groupAssistantMessages: false,
|
|
91
|
+
postProcessUrl: this.postProcessUrl,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// 3. Return the result
|
|
96
|
+
return {
|
|
97
|
+
id: item.id,
|
|
98
|
+
messages,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
/**
|
|
71
103
|
* Remove messages with optional message list return
|
|
72
104
|
* Pattern: delete + conditional query
|
|
@@ -122,38 +154,11 @@ export class MessageService {
|
|
|
122
154
|
}
|
|
123
155
|
|
|
124
156
|
/**
|
|
125
|
-
*
|
|
126
|
-
* Pattern:
|
|
127
|
-
*
|
|
128
|
-
* This method combines message creation and querying into a single operation,
|
|
129
|
-
* reducing the need for separate refresh calls and improving performance.
|
|
157
|
+
* Update message metadata with optional message list return
|
|
158
|
+
* Pattern: update + conditional query
|
|
130
159
|
*/
|
|
131
|
-
async
|
|
132
|
-
|
|
133
|
-
options
|
|
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
|
-
};
|
|
160
|
+
async updateMetadata(id: string, value: any, options?: QueryOptions) {
|
|
161
|
+
await this.messageModel.updateMetadata(id, value);
|
|
162
|
+
return this.queryWithSuccess(options);
|
|
158
163
|
}
|
|
159
164
|
}
|
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
} from '@lobechat/electron-client-ipc';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Desktop notification service
|
|
9
9
|
*/
|
|
10
10
|
export class DesktopNotificationService {
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @param params
|
|
14
|
-
* @returns
|
|
12
|
+
* Show desktop notification (only when window is hidden)
|
|
13
|
+
* @param params Notification parameters
|
|
14
|
+
* @returns Notification result
|
|
15
15
|
*/
|
|
16
16
|
async showNotification(
|
|
17
17
|
params: ShowDesktopNotificationParams,
|
|
@@ -20,8 +20,8 @@ export class DesktopNotificationService {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
* @returns
|
|
23
|
+
* Check if main window is hidden
|
|
24
|
+
* @returns Whether it is hidden
|
|
25
25
|
*/
|
|
26
26
|
async isMainWindowHidden(): Promise<boolean> {
|
|
27
27
|
return dispatch('isMainWindowHidden');
|
|
@@ -2,15 +2,15 @@ import { dispatch } from '@lobechat/electron-client-ipc';
|
|
|
2
2
|
import { FileMetadata } from '@lobechat/types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Desktop application file API client service
|
|
6
6
|
*/
|
|
7
7
|
class DesktopFileAPI {
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @param file
|
|
11
|
-
* @param hash
|
|
12
|
-
* @param path
|
|
13
|
-
* @returns
|
|
9
|
+
* Upload file to desktop application
|
|
10
|
+
* @param file File object
|
|
11
|
+
* @param hash File hash
|
|
12
|
+
* @param path File storage path
|
|
13
|
+
* @returns Upload result
|
|
14
14
|
*/
|
|
15
15
|
async uploadFile(
|
|
16
16
|
file: File,
|
|
@@ -13,9 +13,9 @@ export class BrowserS3Storage {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @param key
|
|
18
|
-
* @param file File
|
|
16
|
+
* Upload file
|
|
17
|
+
* @param key File hash
|
|
18
|
+
* @param file File object
|
|
19
19
|
*/
|
|
20
20
|
putObject = async (key: string, file: File): Promise<void> => {
|
|
21
21
|
try {
|
|
@@ -27,9 +27,9 @@ export class BrowserS3Storage {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param key
|
|
32
|
-
* @returns File
|
|
30
|
+
* Get file
|
|
31
|
+
* @param key File hash
|
|
32
|
+
* @returns File object
|
|
33
33
|
*/
|
|
34
34
|
getObject = async (key: string): Promise<File | undefined> => {
|
|
35
35
|
try {
|
|
@@ -44,8 +44,8 @@ export class BrowserS3Storage {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
* @param key
|
|
47
|
+
* Delete file
|
|
48
|
+
* @param key File hash
|
|
49
49
|
*/
|
|
50
50
|
deleteObject = async (key: string): Promise<void> => {
|
|
51
51
|
try {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { lambdaClient } from '@/libs/trpc/client';
|
|
4
|
+
|
|
5
|
+
import { MessageService } from '../index';
|
|
6
|
+
|
|
7
|
+
vi.mock('@/libs/trpc/client', () => ({
|
|
8
|
+
lambdaClient: {
|
|
9
|
+
message: {
|
|
10
|
+
updateMetadata: {
|
|
11
|
+
mutate: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('MessageService - Race Condition Control', () => {
|
|
18
|
+
let messageService: MessageService;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
messageService = new MessageService();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('updateMessageMetadata race condition', () => {
|
|
26
|
+
it('should cancel previous request when new update is triggered for same message', async () => {
|
|
27
|
+
const messageId = 'test-message-id';
|
|
28
|
+
let firstRequestAborted = false;
|
|
29
|
+
let secondRequestCompleted = false;
|
|
30
|
+
|
|
31
|
+
// Mock first request (slow)
|
|
32
|
+
vi.mocked(lambdaClient.message.updateMetadata.mutate).mockImplementationOnce(
|
|
33
|
+
(_params, options) =>
|
|
34
|
+
new Promise((resolve, reject) => {
|
|
35
|
+
const signal = options?.signal;
|
|
36
|
+
if (signal) {
|
|
37
|
+
signal.addEventListener('abort', () => {
|
|
38
|
+
firstRequestAborted = true;
|
|
39
|
+
reject(new Error('Aborted'));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Simulate slow request
|
|
43
|
+
setTimeout(() => resolve({ success: true, messages: [] }), 200);
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Mock second request (fast)
|
|
48
|
+
vi.mocked(lambdaClient.message.updateMetadata.mutate).mockImplementationOnce(
|
|
49
|
+
async (_params, _options) => {
|
|
50
|
+
secondRequestCompleted = true;
|
|
51
|
+
return { success: true, messages: [] };
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Start first update
|
|
56
|
+
const firstPromise = messageService.updateMessageMetadata(messageId, { compare: true });
|
|
57
|
+
|
|
58
|
+
// Wait a bit then start second update
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
60
|
+
const secondPromise = messageService.updateMessageMetadata(messageId, { compare: false });
|
|
61
|
+
|
|
62
|
+
// First should be aborted
|
|
63
|
+
await expect(firstPromise).rejects.toThrow('Aborted');
|
|
64
|
+
expect(firstRequestAborted).toBe(true);
|
|
65
|
+
|
|
66
|
+
// Second should complete successfully
|
|
67
|
+
await expect(secondPromise).resolves.toEqual({ success: true, messages: [] });
|
|
68
|
+
expect(secondRequestCompleted).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should allow concurrent updates for different messages', async () => {
|
|
72
|
+
const message1Id = 'message-1';
|
|
73
|
+
const message2Id = 'message-2';
|
|
74
|
+
|
|
75
|
+
vi.mocked(lambdaClient.message.updateMetadata.mutate).mockResolvedValue({
|
|
76
|
+
success: true,
|
|
77
|
+
messages: [],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const [result1, result2] = await Promise.all([
|
|
81
|
+
messageService.updateMessageMetadata(message1Id, { cost: 0.001 }),
|
|
82
|
+
messageService.updateMessageMetadata(message2Id, { cost: 0.002 }),
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(result1).toEqual({ success: true, messages: [] });
|
|
86
|
+
expect(result2).toEqual({ success: true, messages: [] });
|
|
87
|
+
expect(lambdaClient.message.updateMetadata.mutate).toHaveBeenCalledTimes(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle rapid successive updates correctly', async () => {
|
|
91
|
+
const messageId = 'test-message-id';
|
|
92
|
+
let completedUpdates = 0;
|
|
93
|
+
const abortedUpdates: number[] = [];
|
|
94
|
+
|
|
95
|
+
// All but the last request should be aborted
|
|
96
|
+
let callIndex = 0;
|
|
97
|
+
vi.mocked(lambdaClient.message.updateMetadata.mutate).mockImplementation(
|
|
98
|
+
(_params, options) => {
|
|
99
|
+
const currentIndex = callIndex++;
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const signal = options?.signal;
|
|
102
|
+
let isAborted = false;
|
|
103
|
+
|
|
104
|
+
if (signal) {
|
|
105
|
+
signal.addEventListener('abort', () => {
|
|
106
|
+
isAborted = true;
|
|
107
|
+
abortedUpdates.push(currentIndex);
|
|
108
|
+
reject(new Error('Aborted'));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
if (!isAborted) {
|
|
114
|
+
completedUpdates++;
|
|
115
|
+
resolve({ success: true, messages: [] });
|
|
116
|
+
}
|
|
117
|
+
}, 50);
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Trigger 5 rapid updates sequentially with catch to prevent unhandled rejections
|
|
123
|
+
const promise1 = messageService
|
|
124
|
+
.updateMessageMetadata(messageId, { cost: 0.001 })
|
|
125
|
+
.catch((e) => e);
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
127
|
+
const promise2 = messageService
|
|
128
|
+
.updateMessageMetadata(messageId, { cost: 0.002 })
|
|
129
|
+
.catch((e) => e);
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
131
|
+
const promise3 = messageService.updateMessageMetadata(messageId, { tps: 10 }).catch((e) => e);
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
133
|
+
const promise4 = messageService.updateMessageMetadata(messageId, { tps: 20 }).catch((e) => e);
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
135
|
+
const promise5 = messageService
|
|
136
|
+
.updateMessageMetadata(messageId, { compare: true })
|
|
137
|
+
.catch((e) => e);
|
|
138
|
+
|
|
139
|
+
// Wait for all to settle
|
|
140
|
+
const results = await Promise.all([promise1, promise2, promise3, promise4, promise5]);
|
|
141
|
+
|
|
142
|
+
// First 4 should be errors (aborted), last should succeed
|
|
143
|
+
expect(results[0]).toBeInstanceOf(Error);
|
|
144
|
+
expect(results[1]).toBeInstanceOf(Error);
|
|
145
|
+
expect(results[2]).toBeInstanceOf(Error);
|
|
146
|
+
expect(results[3]).toBeInstanceOf(Error);
|
|
147
|
+
expect(results[4]).toEqual({ success: true, messages: [] });
|
|
148
|
+
|
|
149
|
+
// 4 requests should have been aborted
|
|
150
|
+
expect(abortedUpdates.length).toBe(4);
|
|
151
|
+
expect(abortedUpdates).toEqual([0, 1, 2, 3]);
|
|
152
|
+
|
|
153
|
+
// Only the last request should complete
|
|
154
|
+
expect(completedUpdates).toBe(1);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ChatTranslate,
|
|
6
6
|
CreateMessageParams,
|
|
7
7
|
CreateMessageResult,
|
|
8
|
+
MessageMetadata,
|
|
8
9
|
ModelRankItem,
|
|
9
10
|
UIChatMessage,
|
|
10
11
|
UpdateMessageParams,
|
|
@@ -15,14 +16,10 @@ import type { HeatmapsProps } from '@lobehub/charts';
|
|
|
15
16
|
|
|
16
17
|
import { INBOX_SESSION_ID } from '@/const/session';
|
|
17
18
|
import { lambdaClient } from '@/libs/trpc/client';
|
|
18
|
-
import { useUserStore } from '@/store/user';
|
|
19
|
-
import { labPreferSelectors } from '@/store/user/selectors';
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
private get useGroup() {
|
|
23
|
-
return labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
|
|
24
|
-
}
|
|
20
|
+
import { abortableRequest } from '../utils/abortableRequest';
|
|
25
21
|
|
|
22
|
+
export class MessageService {
|
|
26
23
|
createMessage = async ({
|
|
27
24
|
sessionId,
|
|
28
25
|
...params
|
|
@@ -30,7 +27,6 @@ export class MessageService {
|
|
|
30
27
|
return lambdaClient.message.createMessage.mutate({
|
|
31
28
|
...params,
|
|
32
29
|
sessionId: sessionId ? this.toDbSessionId(sessionId) : undefined,
|
|
33
|
-
useGroup: this.useGroup,
|
|
34
30
|
});
|
|
35
31
|
};
|
|
36
32
|
|
|
@@ -43,7 +39,6 @@ export class MessageService {
|
|
|
43
39
|
groupId,
|
|
44
40
|
sessionId: this.toDbSessionId(sessionId),
|
|
45
41
|
topicId,
|
|
46
|
-
useGroup: this.useGroup,
|
|
47
42
|
});
|
|
48
43
|
|
|
49
44
|
return data as unknown as UIChatMessage[];
|
|
@@ -53,7 +48,6 @@ export class MessageService {
|
|
|
53
48
|
const data = await lambdaClient.message.getMessages.query({
|
|
54
49
|
groupId,
|
|
55
50
|
topicId,
|
|
56
|
-
useGroup: this.useGroup,
|
|
57
51
|
});
|
|
58
52
|
return data as unknown as UIChatMessage[];
|
|
59
53
|
};
|
|
@@ -100,7 +94,6 @@ export class MessageService {
|
|
|
100
94
|
id,
|
|
101
95
|
sessionId: options?.sessionId,
|
|
102
96
|
topicId: options?.topicId,
|
|
103
|
-
useGroup: this.useGroup,
|
|
104
97
|
value,
|
|
105
98
|
});
|
|
106
99
|
};
|
|
@@ -113,6 +106,24 @@ export class MessageService {
|
|
|
113
106
|
return lambdaClient.message.updateTTS.mutate({ id, value: tts });
|
|
114
107
|
};
|
|
115
108
|
|
|
109
|
+
updateMessageMetadata = async (
|
|
110
|
+
id: string,
|
|
111
|
+
value: Partial<MessageMetadata>,
|
|
112
|
+
options?: { sessionId?: string | null; topicId?: string | null },
|
|
113
|
+
): Promise<UpdateMessageResult> => {
|
|
114
|
+
return abortableRequest.execute(`message-metadata-${id}`, (signal) =>
|
|
115
|
+
lambdaClient.message.updateMetadata.mutate(
|
|
116
|
+
{
|
|
117
|
+
id,
|
|
118
|
+
sessionId: options?.sessionId,
|
|
119
|
+
topicId: options?.topicId,
|
|
120
|
+
value,
|
|
121
|
+
},
|
|
122
|
+
{ signal },
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
116
127
|
updateMessagePluginState = async (
|
|
117
128
|
id: string,
|
|
118
129
|
value: Record<string, any>,
|
|
@@ -122,7 +133,6 @@ export class MessageService {
|
|
|
122
133
|
id,
|
|
123
134
|
sessionId: options?.sessionId,
|
|
124
135
|
topicId: options?.topicId,
|
|
125
|
-
useGroup: this.useGroup,
|
|
126
136
|
value,
|
|
127
137
|
});
|
|
128
138
|
};
|
|
@@ -136,7 +146,6 @@ export class MessageService {
|
|
|
136
146
|
id,
|
|
137
147
|
sessionId: options?.sessionId,
|
|
138
148
|
topicId: options?.topicId,
|
|
139
|
-
useGroup: this.useGroup,
|
|
140
149
|
value: error as any,
|
|
141
150
|
});
|
|
142
151
|
};
|
|
@@ -150,7 +159,6 @@ export class MessageService {
|
|
|
150
159
|
id,
|
|
151
160
|
sessionId: options?.sessionId,
|
|
152
161
|
topicId: options?.topicId,
|
|
153
|
-
useGroup: this.useGroup,
|
|
154
162
|
value: data,
|
|
155
163
|
});
|
|
156
164
|
};
|
|
@@ -163,7 +171,6 @@ export class MessageService {
|
|
|
163
171
|
id,
|
|
164
172
|
sessionId: options?.sessionId,
|
|
165
173
|
topicId: options?.topicId,
|
|
166
|
-
useGroup: this.useGroup,
|
|
167
174
|
});
|
|
168
175
|
};
|
|
169
176
|
|
|
@@ -175,7 +182,6 @@ export class MessageService {
|
|
|
175
182
|
ids,
|
|
176
183
|
sessionId: options?.sessionId,
|
|
177
184
|
topicId: options?.topicId,
|
|
178
|
-
useGroup: this.useGroup,
|
|
179
185
|
});
|
|
180
186
|
};
|
|
181
187
|
|