@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.
Files changed (156) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/next.config.ts +5 -6
  4. package/package.json +2 -2
  5. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
  6. package/packages/agent-runtime/src/core/runtime.ts +63 -18
  7. package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
  8. package/packages/agent-runtime/src/types/index.ts +1 -0
  9. package/packages/agent-runtime/src/types/instruction.ts +10 -3
  10. package/packages/const/src/user.ts +0 -1
  11. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
  12. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
  13. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
  19. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
  22. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
  23. package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
  24. package/packages/conversation-flow/src/index.ts +1 -1
  25. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
  26. package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
  27. package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
  28. package/packages/database/src/models/__tests__/apiKey.test.ts +444 -0
  29. package/packages/database/src/models/message.ts +18 -19
  30. package/packages/types/src/aiChat.ts +2 -0
  31. package/packages/types/src/importer.ts +2 -2
  32. package/packages/types/src/message/ui/chat.ts +17 -1
  33. package/packages/types/src/message/ui/extra.ts +2 -2
  34. package/packages/types/src/message/ui/params.ts +2 -2
  35. package/packages/types/src/user/preference.ts +0 -4
  36. package/packages/utils/src/tokenizer/index.ts +3 -11
  37. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
  38. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
  39. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
  40. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
  41. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
  42. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
  43. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
  44. package/src/app/[variants]/(main)/labs/page.tsx +0 -9
  45. package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
  46. package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
  47. package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
  48. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
  49. package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
  50. package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
  51. package/src/features/Conversation/Error/index.tsx +0 -5
  52. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
  53. package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
  54. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
  55. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
  56. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
  57. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
  58. package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
  59. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
  60. package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
  61. package/src/features/Conversation/Messages/Default.tsx +1 -0
  62. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
  63. package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
  64. package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
  65. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
  66. package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
  67. package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
  68. package/src/features/Conversation/Messages/Group/index.tsx +2 -1
  69. package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
  70. package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
  71. package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
  72. package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
  73. package/src/features/Conversation/Messages/User/index.tsx +43 -44
  74. package/src/features/Conversation/Messages/index.tsx +3 -3
  75. package/src/features/Conversation/components/AutoScroll.tsx +3 -3
  76. package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
  77. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
  78. package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
  79. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
  80. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
  81. package/src/hooks/useHotkeys/chatScope.ts +15 -7
  82. package/src/libs/trpc/client/lambda.ts +4 -3
  83. package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
  84. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
  85. package/src/server/routers/lambda/aiChat.ts +3 -2
  86. package/src/server/routers/lambda/message.ts +8 -16
  87. package/src/server/services/message/__tests__/index.test.ts +29 -39
  88. package/src/server/services/message/index.ts +41 -36
  89. package/src/services/electron/desktopNotification.ts +6 -6
  90. package/src/services/electron/file.ts +6 -6
  91. package/src/services/file/ClientS3/index.ts +8 -8
  92. package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
  93. package/src/services/message/index.ts +21 -15
  94. package/src/services/upload.ts +11 -11
  95. package/src/services/utils/abortableRequest.test.ts +161 -0
  96. package/src/services/utils/abortableRequest.ts +67 -0
  97. package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
  98. package/src/store/chat/agents/createAgentExecutors.ts +395 -0
  99. package/src/store/chat/helpers.test.ts +0 -99
  100. package/src/store/chat/helpers.ts +0 -11
  101. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
  102. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
  103. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
  104. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
  105. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
  106. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
  107. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
  108. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
  109. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
  110. package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
  111. package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
  112. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
  113. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
  114. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
  115. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
  116. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
  117. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  118. package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
  119. package/src/store/chat/slices/message/action.test.ts +79 -68
  120. package/src/store/chat/slices/message/actions/index.ts +39 -0
  121. package/src/store/chat/slices/message/actions/internals.ts +77 -0
  122. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
  123. package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
  124. package/src/store/chat/slices/message/actions/query.ts +120 -0
  125. package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
  126. package/src/store/chat/slices/message/initialState.ts +13 -0
  127. package/src/store/chat/slices/message/reducer.test.ts +48 -370
  128. package/src/store/chat/slices/message/reducer.ts +17 -81
  129. package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
  130. package/src/store/chat/slices/message/selectors/chat.ts +78 -242
  131. package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
  132. package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
  133. package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
  134. package/src/store/chat/slices/plugin/action.test.ts +62 -64
  135. package/src/store/chat/slices/plugin/action.ts +34 -28
  136. package/src/store/chat/slices/thread/action.test.ts +28 -31
  137. package/src/store/chat/slices/thread/action.ts +13 -10
  138. package/src/store/chat/slices/thread/selectors/index.ts +8 -6
  139. package/src/store/chat/slices/topic/reducer.ts +11 -3
  140. package/src/store/chat/store.ts +1 -1
  141. package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
  142. package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
  143. package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
  144. package/packages/database/src/utils/groupMessages.ts +0 -361
  145. package/packages/utils/src/tokenizer/client.ts +0 -35
  146. package/packages/utils/src/tokenizer/estimated.ts +0 -4
  147. package/packages/utils/src/tokenizer/server.ts +0 -11
  148. package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
  149. package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
  150. package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
  151. package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
  152. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
  153. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
  154. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
  155. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
  156. 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.extend({ useGroup: z.boolean().optional() }))
58
+ .input(CreateNewMessageParamsSchema)
59
59
  .mutation(async ({ input, ctx }) => {
60
- const { useGroup, ...params } = input;
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(queryParams, {
90
- groupAssistantMessages: useGroup ?? false,
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
- return ctx.messageModel.updateMetadata(input.id, input.value);
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('useGroup option', () => {
226
- it('should pass useGroup option to query', async () => {
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.removeMessage('msg-1', {
244
+ const result = await messageService.updateMetadata(messageId, metadata, {
231
245
  sessionId: 'session-1',
232
- useGroup: true,
233
246
  });
234
247
 
235
- expect(mockMessageModel.query).toHaveBeenCalledWith(
236
- expect.anything(),
237
- expect.objectContaining({
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 default useGroup to false', async () => {
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.removeMessage('msg-1', { sessionId: 'session-1' });
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
- expect.anything(),
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(options: QueryOptions) {
44
+ private getQueryOptions() {
46
45
  return {
47
- groupAssistantMessages: options.useGroup ?? false,
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(options),
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
- * 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.
157
+ * Update message metadata with optional message list return
158
+ * Pattern: update + conditional query
130
159
  */
131
- async createMessage(
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
- };
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
- * 桌面应用文件API客户端服务
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 文件 hash
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 文件 hash
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 文件 hash
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
- export class MessageService {
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