@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
|
@@ -22,8 +22,8 @@ const mockSet = vi.fn();
|
|
|
22
22
|
|
|
23
23
|
const mockStore = {
|
|
24
24
|
internal_triggerLocalFileToolCalling: vi.fn(),
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
34
|
-
|
|
33
|
+
optimisticUpdateMessageContent: vi.fn(),
|
|
34
|
+
optimisticUpdateMessagePluginError: vi.fn(),
|
|
35
35
|
updatePluginArguments: vi.fn(),
|
|
36
36
|
updatePluginState: vi.fn(),
|
|
37
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
80
|
+
await optimisticUpdateMessageContent(id, JSON.stringify(result));
|
|
81
81
|
await uploadInterpreterFiles(id, result.files);
|
|
82
82
|
} else {
|
|
83
|
-
await
|
|
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 =
|
|
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().
|
|
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().
|
|
312
|
+
await get().optimisticUpdateMessageContent(id, JSON.stringify(content));
|
|
313
313
|
} catch (error) {
|
|
314
|
-
await get().
|
|
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 {
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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().
|
|
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().
|
|
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().
|
|
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
|
|
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(
|
|
75
|
+
expect(updateMessageInputSpy).not.toHaveBeenCalled();
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
it('should call
|
|
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
|
|
95
|
+
it('should call updateMessageInput with empty string', async () => {
|
|
96
96
|
const { result } = renderHook(() => useChatStore());
|
|
97
|
-
const
|
|
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(
|
|
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
|
|
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(
|
|
117
|
+
expect(updateMessageInputSpy).not.toHaveBeenCalled();
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it('should call
|
|
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
|
|
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
|
|
167
|
+
it('should call updateMessageInput with empty string', async () => {
|
|
168
168
|
const { result } = renderHook(() => useChatStore());
|
|
169
|
-
const
|
|
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(
|
|
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
|
|
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:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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('
|
|
471
|
-
it('
|
|
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.
|
|
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.
|
|
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('
|
|
601
|
-
it('should call messageService.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
+
});
|