@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
@@ -22,8 +22,8 @@ const mockSet = vi.fn();
22
22
 
23
23
  const mockStore = {
24
24
  internal_triggerLocalFileToolCalling: vi.fn(),
25
- internal_updateMessageContent: vi.fn(),
26
- internal_updateMessagePluginError: vi.fn(),
25
+ optimisticUpdateMessageContent: vi.fn(),
26
+ optimisticUpdateMessagePluginError: vi.fn(),
27
27
  set: mockSet,
28
28
  toggleLocalFileLoading: vi.fn(),
29
29
  updatePluginArguments: vi.fn(),
@@ -58,7 +58,7 @@ describe('localFileSlice', () => {
58
58
 
59
59
  expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', true);
60
60
  expect(mockStore.updatePluginState).toBeCalledWith('test-id', mockState);
61
- expect(mockStore.internal_updateMessageContent).toBeCalledWith(
61
+ expect(mockStore.optimisticUpdateMessageContent).toBeCalledWith(
62
62
  'test-id',
63
63
  JSON.stringify(mockContent),
64
64
  );
@@ -71,7 +71,7 @@ describe('localFileSlice', () => {
71
71
 
72
72
  await store.internal_triggerLocalFileToolCalling('test-id', mockService);
73
73
 
74
- expect(mockStore.internal_updateMessagePluginError).toBeCalledWith('test-id', {
74
+ expect(mockStore.optimisticUpdateMessagePluginError).toBeCalledWith('test-id', {
75
75
  body: mockError,
76
76
  message: 'test error',
77
77
  type: 'PluginServerError',
@@ -30,11 +30,11 @@ describe('search actions', () => {
30
30
  activeId: 'session-id',
31
31
  activeTopicId: 'topic-id',
32
32
  searchLoading: {},
33
- internal_updateMessageContent: vi.fn(),
34
- internal_updateMessagePluginError: vi.fn(),
33
+ optimisticUpdateMessageContent: vi.fn(),
34
+ optimisticUpdateMessagePluginError: vi.fn(),
35
35
  updatePluginArguments: vi.fn(),
36
36
  updatePluginState: vi.fn(),
37
- internal_createMessage: vi.fn(),
37
+ optimisticCreateMessage: vi.fn(),
38
38
  internal_addToolToAssistantMessage: vi.fn(),
39
39
  openToolUI: vi.fn(),
40
40
  });
@@ -87,7 +87,7 @@ describe('search actions', () => {
87
87
  query: 'test query',
88
88
  });
89
89
  expect(result.current.searchLoading[messageId]).toBe(false);
90
- expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
90
+ expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
91
91
  messageId,
92
92
  searchResultsPrompt(expectedContent),
93
93
  );
@@ -123,7 +123,7 @@ describe('search actions', () => {
123
123
  query: 'test query',
124
124
  });
125
125
  expect(result.current.searchLoading[messageId]).toBe(false);
126
- expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
126
+ expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
127
127
  messageId,
128
128
  searchResultsPrompt([]),
129
129
  );
@@ -145,13 +145,13 @@ describe('search actions', () => {
145
145
  await search(messageId, query);
146
146
  });
147
147
 
148
- expect(result.current.internal_updateMessagePluginError).toHaveBeenCalledWith(messageId, {
148
+ expect(result.current.optimisticUpdateMessagePluginError).toHaveBeenCalledWith(messageId, {
149
149
  body: error,
150
150
  message: 'Search failed',
151
151
  type: 'PluginServerError',
152
152
  });
153
153
  expect(result.current.searchLoading[messageId]).toBe(false);
154
- expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
154
+ expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
155
155
  messageId,
156
156
  'Search failed',
157
157
  );
@@ -190,7 +190,7 @@ describe('search actions', () => {
190
190
  },
191
191
  ];
192
192
 
193
- expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
193
+ expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
194
194
  messageId,
195
195
  crawlResultsPrompt(expectedContent as any),
196
196
  );
@@ -216,7 +216,7 @@ describe('search actions', () => {
216
216
  await result.current.crawlMultiPages(messageId, { urls: ['https://test.com'] });
217
217
  });
218
218
 
219
- expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
219
+ expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
220
220
  messageId,
221
221
  crawlResultsPrompt(mockResponse.results),
222
222
  );
@@ -275,7 +275,7 @@ describe('search actions', () => {
275
275
  await saveSearchResult(messageId);
276
276
  });
277
277
 
278
- expect(result.current.internal_createMessage).toHaveBeenCalledWith(
278
+ expect(result.current.optimisticCreateMessage).toHaveBeenCalledWith(
279
279
  expect.objectContaining({
280
280
  content: 'test content',
281
281
  parentId,
@@ -304,7 +304,7 @@ describe('search actions', () => {
304
304
  await saveSearchResult('non-existent-id');
305
305
  });
306
306
 
307
- expect(result.current.internal_createMessage).not.toHaveBeenCalled();
307
+ expect(result.current.optimisticCreateMessage).not.toHaveBeenCalled();
308
308
  expect(result.current.internal_addToolToAssistantMessage).not.toHaveBeenCalled();
309
309
  });
310
310
  });
@@ -11,7 +11,7 @@ import { StateCreator } from 'zustand/vanilla';
11
11
  import { useClientDataSWR } from '@/libs/swr';
12
12
  import { fileService } from '@/services/file';
13
13
  import { pythonService } from '@/services/python';
14
- import { chatSelectors } from '@/store/chat/selectors';
14
+ import { dbMessageSelectors } from '@/store/chat/selectors';
15
15
  import { ChatStore } from '@/store/chat/store';
16
16
  import { useFileStore } from '@/store/file';
17
17
  import { CodeInterpreterIdentifier } from '@/tools/code-interpreter';
@@ -42,7 +42,7 @@ export const codeInterpreterSlice: StateCreator<
42
42
  const {
43
43
  toggleInterpreterExecuting,
44
44
  updatePluginState,
45
- internal_updateMessageContent,
45
+ optimisticUpdateMessageContent,
46
46
  uploadInterpreterFiles,
47
47
  } = get();
48
48
 
@@ -50,7 +50,7 @@ export const codeInterpreterSlice: StateCreator<
50
50
 
51
51
  // TODO: 应该只下载 AI 用到的文件
52
52
  const files: File[] = [];
53
- for (const message of chatSelectors.mainDisplayChats(get())) {
53
+ for (const message of dbMessageSelectors.dbUserMessages(get())) {
54
54
  for (const file of message.fileList ?? []) {
55
55
  const blob = await fetch(file.url).then((res) => res.blob());
56
56
  files.push(new File([blob], file.name));
@@ -61,7 +61,7 @@ export const codeInterpreterSlice: StateCreator<
61
61
  }
62
62
  for (const tool of message.tools ?? []) {
63
63
  if (tool.identifier === CodeInterpreterIdentifier) {
64
- const message = chatSelectors.getMessageByToolCallId(tool.id)(get());
64
+ const message = dbMessageSelectors.getDbMessageByToolCallId(tool.id)(get());
65
65
  if (message?.content) {
66
66
  const content = JSON.parse(message.content) as CodeInterpreterResponse;
67
67
  for (const file of content.files ?? []) {
@@ -77,10 +77,10 @@ export const codeInterpreterSlice: StateCreator<
77
77
  try {
78
78
  const result = await pythonService.runPython(params.code, params.packages, files);
79
79
  if (result?.files) {
80
- await internal_updateMessageContent(id, JSON.stringify(result));
80
+ await optimisticUpdateMessageContent(id, JSON.stringify(result));
81
81
  await uploadInterpreterFiles(id, result.files);
82
82
  } else {
83
- await internal_updateMessageContent(id, JSON.stringify(result));
83
+ await optimisticUpdateMessageContent(id, JSON.stringify(result));
84
84
  }
85
85
  } catch (error) {
86
86
  updatePluginState(id, { error });
@@ -105,7 +105,7 @@ export const codeInterpreterSlice: StateCreator<
105
105
  id: string,
106
106
  updater: (data: CodeInterpreterResponse) => void,
107
107
  ) => {
108
- const message = chatSelectors.getMessageById(id)(get());
108
+ const message = dbMessageSelectors.getDbMessageById(id)(get());
109
109
  if (!message) return;
110
110
 
111
111
  const result: CodeInterpreterResponse = JSON.parse(message.content);
@@ -113,7 +113,7 @@ export const codeInterpreterSlice: StateCreator<
113
113
 
114
114
  const nextResult = produce(result, updater);
115
115
 
116
- await get().internal_updateMessageContent(id, JSON.stringify(nextResult));
116
+ await get().optimisticUpdateMessageContent(id, JSON.stringify(nextResult));
117
117
  },
118
118
 
119
119
  uploadInterpreterFiles: async (id: string, files: CodeInterpreterFileItem[]) => {
@@ -309,9 +309,9 @@ export const localSystemSlice: StateCreator<
309
309
  if (state) {
310
310
  await get().updatePluginState(id, state as any);
311
311
  }
312
- await get().internal_updateMessageContent(id, JSON.stringify(content));
312
+ await get().optimisticUpdateMessageContent(id, JSON.stringify(content));
313
313
  } catch (error) {
314
- await get().internal_updateMessagePluginError(id, {
314
+ await get().optimisticUpdateMessagePluginError(id, {
315
315
  body: error,
316
316
  message: (error as Error).message,
317
317
  type: 'PluginServerError',
@@ -43,12 +43,12 @@ export const searchSlice: StateCreator<
43
43
  SearchAction
44
44
  > = (set, get) => ({
45
45
  crawlMultiPages: async (id, params, aiSummary = true) => {
46
- const { internal_updateMessageContent } = get();
46
+ const { optimisticUpdateMessageContent } = get();
47
47
  get().toggleSearchLoading(id, true);
48
48
  try {
49
49
  const { content, success, error, state } = await runtime.crawlMultiPages(params);
50
50
 
51
- await internal_updateMessageContent(id, content);
51
+ await optimisticUpdateMessageContent(id, content);
52
52
 
53
53
  if (success) {
54
54
  await get().updatePluginState(id, state);
@@ -67,7 +67,7 @@ export const searchSlice: StateCreator<
67
67
  const content = [{ errorMessage: err.message, errorType: err.name }];
68
68
 
69
69
  const xmlContent = crawlResultsPrompt(content);
70
- await internal_updateMessageContent(id, xmlContent);
70
+ await optimisticUpdateMessageContent(id, xmlContent);
71
71
  }
72
72
  },
73
73
 
@@ -81,7 +81,7 @@ export const searchSlice: StateCreator<
81
81
  const message = chatSelectors.getMessageById(id)(get());
82
82
  if (!message || !message.plugin) return;
83
83
 
84
- const { internal_addToolToAssistantMessage, internal_createMessage, openToolUI } = get();
84
+ const { internal_addToolToAssistantMessage, optimisticCreateMessage, openToolUI } = get();
85
85
  // 1. 创建一个新的 tool call message
86
86
  const newToolCallId = `tool_call_${nanoid()}`;
87
87
 
@@ -108,7 +108,7 @@ export const searchSlice: StateCreator<
108
108
 
109
109
  const [result] = await Promise.all([
110
110
  // 1. 添加 tool message
111
- internal_createMessage(toolMessage),
111
+ optimisticCreateMessage(toolMessage),
112
112
  // 2. 将这条 tool call message 插入到 ai 消息的 tools 中
113
113
  addToolItem(),
114
114
  ]);
@@ -127,7 +127,7 @@ export const searchSlice: StateCreator<
127
127
  await get().updatePluginState(id, state);
128
128
  } else {
129
129
  if ((error as Error).message === SEARCH_SEARXNG_NOT_CONFIG) {
130
- await get().internal_updateMessagePluginError(id, {
130
+ await get().optimisticUpdateMessagePluginError(id, {
131
131
  body: {
132
132
  provider: 'searxng',
133
133
  },
@@ -135,7 +135,7 @@ export const searchSlice: StateCreator<
135
135
  type: 'PluginSettingsInvalid',
136
136
  });
137
137
  } else {
138
- await get().internal_updateMessagePluginError(id, {
138
+ await get().optimisticUpdateMessagePluginError(id, {
139
139
  body: error,
140
140
  message: (error as Error).message,
141
141
  type: 'PluginServerError',
@@ -145,7 +145,7 @@ export const searchSlice: StateCreator<
145
145
 
146
146
  get().toggleSearchLoading(id, false);
147
147
 
148
- await get().internal_updateMessageContent(id, content);
148
+ await get().optimisticUpdateMessageContent(id, content);
149
149
 
150
150
  // 如果 aiSummary 为 true,则会自动触发总结
151
151
  return aiSummary;
@@ -65,17 +65,17 @@ describe('chatMessage actions', () => {
65
65
  it('should return early if activeId is undefined', async () => {
66
66
  useChatStore.setState({ activeId: undefined });
67
67
  const { result } = renderHook(() => useChatStore());
68
- const updateInputMessageSpy = vi.spyOn(result.current, 'updateInputMessage');
68
+ const updateMessageInputSpy = vi.spyOn(result.current, 'updateMessageInput');
69
69
 
70
70
  await act(async () => {
71
71
  await result.current.addAIMessage();
72
72
  });
73
73
 
74
74
  expect(messageService.createMessage).not.toHaveBeenCalled();
75
- expect(updateInputMessageSpy).not.toHaveBeenCalled();
75
+ expect(updateMessageInputSpy).not.toHaveBeenCalled();
76
76
  });
77
77
 
78
- it('should call internal_createMessage with correct parameters', async () => {
78
+ it('should call optimisticCreateMessage with correct parameters', async () => {
79
79
  const inputMessage = 'Test input message';
80
80
  useChatStore.setState({ inputMessage });
81
81
  const { result } = renderHook(() => useChatStore());
@@ -92,14 +92,14 @@ describe('chatMessage actions', () => {
92
92
  });
93
93
  });
94
94
 
95
- it('should call updateInputMessage with empty string', async () => {
95
+ it('should call updateMessageInput with empty string', async () => {
96
96
  const { result } = renderHook(() => useChatStore());
97
- const updateInputMessageSpy = vi.spyOn(result.current, 'updateInputMessage');
97
+ const updateMessageInputSpy = vi.spyOn(result.current, 'updateMessageInput');
98
98
  await act(async () => {
99
99
  await result.current.addAIMessage();
100
100
  });
101
101
 
102
- expect(updateInputMessageSpy).toHaveBeenCalledWith('');
102
+ expect(updateMessageInputSpy).toHaveBeenCalledWith('');
103
103
  });
104
104
  });
105
105
 
@@ -107,17 +107,17 @@ describe('chatMessage actions', () => {
107
107
  it('should return early if activeId is undefined', async () => {
108
108
  useChatStore.setState({ activeId: undefined });
109
109
  const { result } = renderHook(() => useChatStore());
110
- const updateInputMessageSpy = vi.spyOn(result.current, 'updateInputMessage');
110
+ const updateMessageInputSpy = vi.spyOn(result.current, 'updateMessageInput');
111
111
 
112
112
  await act(async () => {
113
113
  await result.current.addUserMessage({ message: 'test message' });
114
114
  });
115
115
 
116
116
  expect(messageService.createMessage).not.toHaveBeenCalled();
117
- expect(updateInputMessageSpy).not.toHaveBeenCalled();
117
+ expect(updateMessageInputSpy).not.toHaveBeenCalled();
118
118
  });
119
119
 
120
- it('should call internal_createMessage with correct parameters', async () => {
120
+ it('should call optimisticCreateMessage with correct parameters', async () => {
121
121
  const message = 'Test user message';
122
122
  const fileList = ['file-id-1', 'file-id-2'];
123
123
  useChatStore.setState({
@@ -140,7 +140,7 @@ describe('chatMessage actions', () => {
140
140
  });
141
141
  });
142
142
 
143
- it('should call internal_createMessage with threadId when activeThreadId is set', async () => {
143
+ it('should call optimisticCreateMessage with threadId when activeThreadId is set', async () => {
144
144
  const message = 'Test user message';
145
145
  const activeThreadId = 'thread-123';
146
146
  useChatStore.setState({
@@ -164,15 +164,15 @@ describe('chatMessage actions', () => {
164
164
  });
165
165
  });
166
166
 
167
- it('should call updateInputMessage with empty string', async () => {
167
+ it('should call updateMessageInput with empty string', async () => {
168
168
  const { result } = renderHook(() => useChatStore());
169
- const updateInputMessageSpy = vi.spyOn(result.current, 'updateInputMessage');
169
+ const updateMessageInputSpy = vi.spyOn(result.current, 'updateMessageInput');
170
170
 
171
171
  await act(async () => {
172
172
  await result.current.addUserMessage({ message: 'test' });
173
173
  });
174
174
 
175
- expect(updateInputMessageSpy).toHaveBeenCalledWith('');
175
+ expect(updateMessageInputSpy).toHaveBeenCalledWith('');
176
176
  });
177
177
 
178
178
  it('should handle message without fileList', async () => {
@@ -265,7 +265,7 @@ describe('chatMessage actions', () => {
265
265
  expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages);
266
266
  });
267
267
 
268
- it('deleteMessage should remove group message with all children', async () => {
268
+ it('deleteMessage should remove assistantGroup message with all children', async () => {
269
269
  const { result } = renderHook(() => useChatStore());
270
270
  const groupMessageId = 'group-message-id';
271
271
  const removeMessagesSpy = vi.spyOn(messageService, 'removeMessages');
@@ -285,18 +285,20 @@ describe('chatMessage actions', () => {
285
285
  activeTopicId: undefined,
286
286
  messagesMap: {
287
287
  [messageMapKey('session-id')]: [
288
- { id: groupMessageId, role: 'group', content: 'Group message' } as UIChatMessage,
289
288
  {
290
- id: 'child-1',
291
- parentId: groupMessageId,
292
- role: 'assistant',
293
- content: 'Child 1',
294
- } as UIChatMessage,
295
- {
296
- id: 'child-2',
297
- parentId: groupMessageId,
298
- role: 'assistant',
299
- content: 'Child 2',
289
+ id: groupMessageId,
290
+ role: 'assistantGroup',
291
+ content: '',
292
+ children: [
293
+ {
294
+ id: 'child-1',
295
+ content: 'Child 1',
296
+ },
297
+ {
298
+ id: 'child-2',
299
+ content: 'Child 2',
300
+ },
301
+ ],
300
302
  } as UIChatMessage,
301
303
  { id: 'other-message', role: 'user', content: 'Other' } as UIChatMessage,
302
304
  ],
@@ -334,25 +336,29 @@ describe('chatMessage actions', () => {
334
336
  activeTopicId: undefined,
335
337
  messagesMap: {
336
338
  [messageMapKey('session-id')]: [
337
- { id: groupMessageId, role: 'group', content: 'Group message' } as UIChatMessage,
338
- {
339
- id: 'child-1',
340
- parentId: groupMessageId,
341
- role: 'assistant',
342
- content: 'Child with tools',
343
- tools: [{ id: 'tool1' }],
344
- } as UIChatMessage,
345
339
  {
346
- id: 'tool-result-1',
347
- tool_call_id: 'tool1',
348
- role: 'tool',
349
- content: 'Tool result',
350
- } as UIChatMessage,
351
- {
352
- id: 'child-2',
353
- parentId: groupMessageId,
354
- role: 'assistant',
355
- content: 'Child 2',
340
+ id: groupMessageId,
341
+ role: 'assistantGroup',
342
+ content: '',
343
+ children: [
344
+ {
345
+ id: 'child-1',
346
+ content: 'Child with tools',
347
+ tools: [
348
+ {
349
+ id: 'tool1',
350
+ result: {
351
+ id: 'tool-result-1',
352
+ content: 'Tool result',
353
+ },
354
+ },
355
+ ],
356
+ },
357
+ {
358
+ id: 'child-2',
359
+ content: 'Child 2',
360
+ },
361
+ ],
356
362
  } as UIChatMessage,
357
363
  { id: 'other-message', role: 'user', content: 'Other' } as UIChatMessage,
358
364
  ],
@@ -363,7 +369,7 @@ describe('chatMessage actions', () => {
363
369
  await result.current.deleteMessage(groupMessageId);
364
370
  });
365
371
 
366
- // Should delete group message + all children + tool results of children
372
+ // Should delete assistantGroup message + all children + tool results of children
367
373
  expect(removeMessagesSpy).toHaveBeenCalledWith(
368
374
  [groupMessageId, 'child-1', 'child-2', 'tool-result-1'],
369
375
  {
@@ -413,24 +419,29 @@ describe('chatMessage actions', () => {
413
419
  const removeMessageSpy = vi.spyOn(messageService, 'removeMessage');
414
420
 
415
421
  act(() => {
422
+ const rawMessages = [
423
+ {
424
+ id: messageId,
425
+ role: 'assistant',
426
+ tools: [{ id: 'tool1' }, { id: 'tool2' }],
427
+ } as UIChatMessage,
428
+ {
429
+ id: '2',
430
+ parentId: messageId,
431
+ tool_call_id: 'tool1',
432
+ role: 'tool',
433
+ } as UIChatMessage,
434
+ { id: '3', tool_call_id: 'tool2', role: 'tool' } as UIChatMessage,
435
+ ];
436
+
416
437
  useChatStore.setState({
417
438
  activeId: 'session-id',
418
439
  activeTopicId: undefined,
440
+ dbMessagesMap: {
441
+ [messageMapKey('session-id')]: rawMessages,
442
+ },
419
443
  messagesMap: {
420
- [messageMapKey('session-id')]: [
421
- {
422
- id: messageId,
423
- role: 'assistant',
424
- tools: [{ id: 'tool1' }, { id: 'tool2' }],
425
- } as UIChatMessage,
426
- {
427
- id: '2',
428
- parentId: messageId,
429
- tool_call_id: 'tool1',
430
- role: 'tool',
431
- } as UIChatMessage,
432
- { id: '3', tool_call_id: 'tool2', role: 'tool' } as UIChatMessage,
433
- ],
444
+ [messageMapKey('session-id')]: rawMessages,
434
445
  },
435
446
  });
436
447
  });
@@ -467,12 +478,12 @@ describe('chatMessage actions', () => {
467
478
  });
468
479
  });
469
480
 
470
- describe('updateInputMessage', () => {
471
- it('updateInputMessage should update the input message state', () => {
481
+ describe('updateMessageInput', () => {
482
+ it('updateMessageInput should update the input message state', () => {
472
483
  const { result } = renderHook(() => useChatStore());
473
484
  const newInputMessage = 'Updated message';
474
485
  act(() => {
475
- result.current.updateInputMessage(newInputMessage);
486
+ result.current.updateMessageInput(newInputMessage);
476
487
  });
477
488
 
478
489
  expect(result.current.inputMessage).toEqual(newInputMessage);
@@ -484,7 +495,7 @@ describe('chatMessage actions', () => {
484
495
  const { result } = renderHook(() => useChatStore());
485
496
 
486
497
  act(() => {
487
- result.current.updateInputMessage(inputMessage);
498
+ result.current.updateMessageInput(inputMessage);
488
499
  });
489
500
 
490
501
  expect(result.current.inputMessage).toBe(inputMessage);
@@ -597,15 +608,15 @@ describe('chatMessage actions', () => {
597
608
  });
598
609
  });
599
610
 
600
- describe('internal_updateMessageContent', () => {
601
- it('should call messageService.internal_updateMessageContent with correct parameters', async () => {
611
+ describe('optimisticUpdateMessageContent', () => {
612
+ it('should call messageService.optimisticUpdateMessageContent with correct parameters', async () => {
602
613
  const { result } = renderHook(() => useChatStore());
603
614
  const messageId = 'message-id';
604
615
  const newContent = 'Updated content';
605
616
 
606
617
  const spy = vi.spyOn(messageService, 'updateMessage');
607
618
  await act(async () => {
608
- await result.current.internal_updateMessageContent(messageId, newContent);
619
+ await result.current.optimisticUpdateMessageContent(messageId, newContent);
609
620
  });
610
621
 
611
622
  expect(spy).toHaveBeenCalledWith(
@@ -622,7 +633,7 @@ describe('chatMessage actions', () => {
622
633
  const internal_dispatchMessageSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
623
634
 
624
635
  await act(async () => {
625
- await result.current.internal_updateMessageContent(messageId, newContent);
636
+ await result.current.optimisticUpdateMessageContent(messageId, newContent);
626
637
  });
627
638
 
628
639
  expect(internal_dispatchMessageSpy).toHaveBeenCalledWith({
@@ -638,7 +649,7 @@ describe('chatMessage actions', () => {
638
649
  const newContent = 'Updated content';
639
650
 
640
651
  await act(async () => {
641
- await result.current.internal_updateMessageContent(messageId, newContent);
652
+ await result.current.optimisticUpdateMessageContent(messageId, newContent);
642
653
  });
643
654
 
644
655
  expect(result.current.refreshMessages).toHaveBeenCalled();
@@ -770,7 +781,7 @@ describe('chatMessage actions', () => {
770
781
  });
771
782
  });
772
783
 
773
- it('should call internal_updateMessageContent with correct parameters', async () => {
784
+ it('should call optimisticUpdateMessageContent with correct parameters', async () => {
774
785
  const messageId = 'message-id';
775
786
  const content = 'Updated content';
776
787
  const { result } = renderHook(() => useChatStore());
@@ -0,0 +1,39 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { ChatStore } from '@/store/chat/store';
4
+
5
+ import { MessageInternalsAction, messageInternals } from './internals';
6
+ import { MessageOptimisticUpdateAction, messageOptimisticUpdate } from './optimisticUpdate';
7
+ import { MessagePublicApiAction, messagePublicApi } from './publicApi';
8
+ import { MessageQueryAction, messageQuery } from './query';
9
+ import { MessageRuntimeStateAction, messageRuntimeState } from './runtimeState';
10
+
11
+ /**
12
+ * Combined message action interface
13
+ * Aggregates all message-related actions
14
+ */
15
+ export interface ChatMessageAction
16
+ extends MessagePublicApiAction,
17
+ MessageOptimisticUpdateAction,
18
+ MessageQueryAction,
19
+ MessageRuntimeStateAction,
20
+ MessageInternalsAction {
21
+ /**/
22
+ }
23
+
24
+ /**
25
+ * Combined message action creator
26
+ * Merges all message action modules
27
+ */
28
+ export const chatMessage: StateCreator<
29
+ ChatStore,
30
+ [['zustand/devtools', never]],
31
+ [],
32
+ ChatMessageAction
33
+ > = (...params) => ({
34
+ ...messagePublicApi(...params),
35
+ ...messageOptimisticUpdate(...params),
36
+ ...messageQuery(...params),
37
+ ...messageRuntimeState(...params),
38
+ ...messageInternals(...params),
39
+ });
@@ -0,0 +1,77 @@
1
+ import { parse } from '@lobechat/conversation-flow';
2
+ import { TraceEventPayloads } from '@lobechat/types';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { StateCreator } from 'zustand/vanilla';
5
+
6
+ import { traceService } from '@/services/trace';
7
+ import { ChatStore } from '@/store/chat/store';
8
+
9
+ import { displayMessageSelectors } from '../../../selectors';
10
+ import { messageMapKey } from '../../../utils/messageMapKey';
11
+ import { MessageDispatch, messagesReducer } from '../reducer';
12
+
13
+ /**
14
+ * Internal core methods that serve as building blocks for other actions
15
+ */
16
+ export interface MessageInternalsAction {
17
+ /**
18
+ * update message at the frontend
19
+ * this method will not update messages to database
20
+ */
21
+ internal_dispatchMessage: (
22
+ payload: MessageDispatch,
23
+ context?: { sessionId: string; topicId?: string | null },
24
+ ) => void;
25
+
26
+ /**
27
+ * trace message events for analytics
28
+ */
29
+ internal_traceMessage: (id: string, payload: TraceEventPayloads) => Promise<void>;
30
+ }
31
+
32
+ export const messageInternals: StateCreator<
33
+ ChatStore,
34
+ [['zustand/devtools', never]],
35
+ [],
36
+ MessageInternalsAction
37
+ > = (set, get) => ({
38
+ // the internal process method of the AI message
39
+ internal_dispatchMessage: (payload, context) => {
40
+ const activeId = typeof context !== 'undefined' ? context.sessionId : get().activeId;
41
+ const topicId = typeof context !== 'undefined' ? context.topicId : get().activeTopicId;
42
+
43
+ const messagesKey = messageMapKey(activeId, topicId);
44
+
45
+ // Get raw messages from dbMessagesMap and apply reducer
46
+ const rawMessages = get().dbMessagesMap[messagesKey] || [];
47
+ const updatedRawMessages = messagesReducer(rawMessages, payload);
48
+
49
+ const nextDbMap = { ...get().dbMessagesMap, [messagesKey]: updatedRawMessages };
50
+
51
+ if (isEqual(nextDbMap, get().dbMessagesMap)) return;
52
+
53
+ // parse to get display messages
54
+ const { flatList } = parse(updatedRawMessages);
55
+ const nextDisplayMap = { ...get().messagesMap, [messagesKey]: flatList };
56
+
57
+ set({ dbMessagesMap: nextDbMap, messagesMap: nextDisplayMap }, false, {
58
+ payload,
59
+ type: `dispatchMessage/${payload.type}`,
60
+ });
61
+ },
62
+
63
+ internal_traceMessage: async (id, payload) => {
64
+ // tracing the diff of update
65
+ const message = displayMessageSelectors.getDisplayMessageById(id)(get());
66
+ if (!message) return;
67
+
68
+ const traceId = message?.traceId;
69
+ const observationId = message?.observationId;
70
+
71
+ if (traceId && message?.role === 'assistant') {
72
+ traceService
73
+ .traceEvent({ content: message.content, observationId, traceId, ...payload })
74
+ .catch();
75
+ }
76
+ },
77
+ });