@lobehub/lobehub 2.0.0-next.84 → 2.0.0-next.86
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/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
- package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
- package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
- package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/runtime.ts +36 -1
- package/packages/agent-runtime/src/types/event.ts +1 -0
- package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
- package/packages/agent-runtime/src/types/instruction.ts +30 -0
- package/packages/agent-runtime/src/types/runtime.ts +7 -0
- package/packages/types/src/message/common/metadata.ts +3 -0
- package/packages/types/src/message/common/tools.ts +2 -2
- package/packages/types/src/tool/search/index.ts +8 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
- package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
- package/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +1 -1
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
- package/src/features/Conversation/Messages/User/index.tsx +3 -3
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +2 -2
- package/src/services/search.ts +2 -2
- package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
- package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
- package/src/store/chat/agents/createAgentExecutors.ts +313 -80
- package/src/store/chat/selectors.ts +1 -0
- package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
- package/src/store/chat/slices/aiChat/initialState.ts +0 -28
- package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
- package/src/store/chat/slices/aiChat/selectors.ts +31 -7
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
- package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
- package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
- package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
- package/src/store/chat/slices/message/action.test.ts +134 -16
- package/src/store/chat/slices/message/actions/internals.ts +33 -7
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
- package/src/store/chat/slices/message/initialState.ts +0 -10
- package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
- package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
- package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
- package/src/store/chat/slices/operation/actions.ts +218 -11
- package/src/store/chat/slices/operation/selectors.ts +135 -6
- package/src/store/chat/slices/operation/types.ts +29 -3
- package/src/store/chat/slices/plugin/action.test.ts +30 -322
- package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
- package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
- package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
- package/src/store/chat/slices/thread/selectors/index.ts +4 -2
- package/src/store/chat/slices/translate/action.ts +54 -41
- package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
- package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { useChatStore } from '@/store/chat/store';
|
|
5
|
+
|
|
6
|
+
import { chatToolSelectors } from './selectors';
|
|
7
|
+
|
|
8
|
+
describe('chatToolSelectors', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
useChatStore.setState(useChatStore.getInitialState());
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('isDallEImageGenerating', () => {
|
|
14
|
+
it('should return true when DALL-E image is generating for message', () => {
|
|
15
|
+
const { result } = renderHook(() => useChatStore());
|
|
16
|
+
|
|
17
|
+
act(() => {
|
|
18
|
+
useChatStore.setState({
|
|
19
|
+
dalleImageLoading: {
|
|
20
|
+
msg1: true,
|
|
21
|
+
msg2: false,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(chatToolSelectors.isDallEImageGenerating('msg1')(result.current)).toBe(true);
|
|
27
|
+
expect(chatToolSelectors.isDallEImageGenerating('msg2')(result.current)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return undefined when message not in loading state', () => {
|
|
31
|
+
const { result } = renderHook(() => useChatStore());
|
|
32
|
+
|
|
33
|
+
expect(chatToolSelectors.isDallEImageGenerating('msg1')(result.current)).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('isGeneratingDallEImage', () => {
|
|
38
|
+
it('should return true when any DALL-E image is generating', () => {
|
|
39
|
+
const { result } = renderHook(() => useChatStore());
|
|
40
|
+
|
|
41
|
+
act(() => {
|
|
42
|
+
useChatStore.setState({
|
|
43
|
+
dalleImageLoading: {
|
|
44
|
+
msg1: false,
|
|
45
|
+
msg2: true,
|
|
46
|
+
msg3: false,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(chatToolSelectors.isGeneratingDallEImage(result.current)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return false when no DALL-E images are generating', () => {
|
|
55
|
+
const { result } = renderHook(() => useChatStore());
|
|
56
|
+
|
|
57
|
+
act(() => {
|
|
58
|
+
useChatStore.setState({
|
|
59
|
+
dalleImageLoading: {
|
|
60
|
+
msg1: false,
|
|
61
|
+
msg2: false,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(chatToolSelectors.isGeneratingDallEImage(result.current)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return false when dalleImageLoading is empty', () => {
|
|
70
|
+
const { result } = renderHook(() => useChatStore());
|
|
71
|
+
|
|
72
|
+
expect(chatToolSelectors.isGeneratingDallEImage(result.current)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('isInterpreterExecuting', () => {
|
|
77
|
+
it('should return true when interpreter is executing for message', () => {
|
|
78
|
+
const { result } = renderHook(() => useChatStore());
|
|
79
|
+
|
|
80
|
+
let opId: string;
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
opId = result.current.startOperation({
|
|
84
|
+
type: 'builtinToolInterpreter',
|
|
85
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
86
|
+
}).operationId;
|
|
87
|
+
|
|
88
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(chatToolSelectors.isInterpreterExecuting('msg1')(result.current)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return false when no operation exists for message', () => {
|
|
95
|
+
const { result } = renderHook(() => useChatStore());
|
|
96
|
+
|
|
97
|
+
expect(chatToolSelectors.isInterpreterExecuting('msg1')(result.current)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return false when operation is not builtinToolInterpreter', () => {
|
|
101
|
+
const { result } = renderHook(() => useChatStore());
|
|
102
|
+
|
|
103
|
+
let opId: string;
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
opId = result.current.startOperation({
|
|
107
|
+
type: 'execAgentRuntime',
|
|
108
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
109
|
+
}).operationId;
|
|
110
|
+
|
|
111
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(chatToolSelectors.isInterpreterExecuting('msg1')(result.current)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return false when operation is completed', () => {
|
|
118
|
+
const { result } = renderHook(() => useChatStore());
|
|
119
|
+
|
|
120
|
+
let opId: string;
|
|
121
|
+
|
|
122
|
+
act(() => {
|
|
123
|
+
opId = result.current.startOperation({
|
|
124
|
+
type: 'builtinToolInterpreter',
|
|
125
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
126
|
+
}).operationId;
|
|
127
|
+
|
|
128
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
act(() => {
|
|
132
|
+
result.current.completeOperation(opId);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(chatToolSelectors.isInterpreterExecuting('msg1')(result.current)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('isSearXNGSearching', () => {
|
|
140
|
+
it('should return true when SearXNG search is running for message', () => {
|
|
141
|
+
const { result } = renderHook(() => useChatStore());
|
|
142
|
+
|
|
143
|
+
let opId: string;
|
|
144
|
+
|
|
145
|
+
act(() => {
|
|
146
|
+
opId = result.current.startOperation({
|
|
147
|
+
type: 'builtinToolSearch',
|
|
148
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
149
|
+
}).operationId;
|
|
150
|
+
|
|
151
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(chatToolSelectors.isSearXNGSearching('msg1')(result.current)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return false when no operation exists', () => {
|
|
158
|
+
const { result } = renderHook(() => useChatStore());
|
|
159
|
+
|
|
160
|
+
expect(chatToolSelectors.isSearXNGSearching('msg1')(result.current)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return false when operation type is different', () => {
|
|
164
|
+
const { result } = renderHook(() => useChatStore());
|
|
165
|
+
|
|
166
|
+
let opId: string;
|
|
167
|
+
|
|
168
|
+
act(() => {
|
|
169
|
+
opId = result.current.startOperation({
|
|
170
|
+
type: 'builtinToolInterpreter',
|
|
171
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
172
|
+
}).operationId;
|
|
173
|
+
|
|
174
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(chatToolSelectors.isSearXNGSearching('msg1')(result.current)).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return false when operation is not running', () => {
|
|
181
|
+
const { result } = renderHook(() => useChatStore());
|
|
182
|
+
|
|
183
|
+
let opId: string;
|
|
184
|
+
|
|
185
|
+
act(() => {
|
|
186
|
+
opId = result.current.startOperation({
|
|
187
|
+
type: 'builtinToolSearch',
|
|
188
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
189
|
+
}).operationId;
|
|
190
|
+
|
|
191
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
192
|
+
result.current.completeOperation(opId);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(chatToolSelectors.isSearXNGSearching('msg1')(result.current)).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('isSearchingLocalFiles', () => {
|
|
200
|
+
it('should return true when local system search is running', () => {
|
|
201
|
+
const { result } = renderHook(() => useChatStore());
|
|
202
|
+
|
|
203
|
+
let opId: string;
|
|
204
|
+
|
|
205
|
+
act(() => {
|
|
206
|
+
opId = result.current.startOperation({
|
|
207
|
+
type: 'builtinToolLocalSystem',
|
|
208
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
209
|
+
}).operationId;
|
|
210
|
+
|
|
211
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(chatToolSelectors.isSearchingLocalFiles('msg1')(result.current)).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should return false when no operation exists', () => {
|
|
218
|
+
const { result } = renderHook(() => useChatStore());
|
|
219
|
+
|
|
220
|
+
expect(chatToolSelectors.isSearchingLocalFiles('msg1')(result.current)).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should return false when operation type is different', () => {
|
|
224
|
+
const { result } = renderHook(() => useChatStore());
|
|
225
|
+
|
|
226
|
+
let opId: string;
|
|
227
|
+
|
|
228
|
+
act(() => {
|
|
229
|
+
opId = result.current.startOperation({
|
|
230
|
+
type: 'builtinToolSearch',
|
|
231
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
232
|
+
}).operationId;
|
|
233
|
+
|
|
234
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(chatToolSelectors.isSearchingLocalFiles('msg1')(result.current)).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should return false when operation is completed', () => {
|
|
241
|
+
const { result } = renderHook(() => useChatStore());
|
|
242
|
+
|
|
243
|
+
let opId: string;
|
|
244
|
+
|
|
245
|
+
act(() => {
|
|
246
|
+
opId = result.current.startOperation({
|
|
247
|
+
type: 'builtinToolLocalSystem',
|
|
248
|
+
context: { sessionId: 'session1', messageId: 'msg1' },
|
|
249
|
+
}).operationId;
|
|
250
|
+
|
|
251
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
252
|
+
result.current.completeOperation(opId);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(chatToolSelectors.isSearchingLocalFiles('msg1')(result.current)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -5,11 +5,32 @@ const isDallEImageGenerating = (id: string) => (s: ChatStoreState) => s.dalleIma
|
|
|
5
5
|
const isGeneratingDallEImage = (s: ChatStoreState) =>
|
|
6
6
|
Object.values(s.dalleImageLoading).some(Boolean);
|
|
7
7
|
|
|
8
|
-
const isInterpreterExecuting = (id: string) => (s: ChatStoreState) =>
|
|
9
|
-
s
|
|
8
|
+
const isInterpreterExecuting = (id: string) => (s: ChatStoreState) => {
|
|
9
|
+
// Check if there's a running builtinToolInterpreter operation for this message
|
|
10
|
+
const operationId = s.messageOperationMap[id];
|
|
11
|
+
if (!operationId) return false;
|
|
10
12
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
+
const operation = s.operations[operationId];
|
|
14
|
+
return operation?.type === 'builtinToolInterpreter' && operation?.status === 'running';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const isSearXNGSearching = (id: string) => (s: ChatStoreState) => {
|
|
18
|
+
// Check if there's a running builtinToolSearch operation for this message
|
|
19
|
+
const operationId = s.messageOperationMap[id];
|
|
20
|
+
if (!operationId) return false;
|
|
21
|
+
|
|
22
|
+
const operation = s.operations[operationId];
|
|
23
|
+
return operation?.type === 'builtinToolSearch' && operation?.status === 'running';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const isSearchingLocalFiles = (id: string) => (s: ChatStoreState) => {
|
|
27
|
+
// Check if there's a running builtinToolLocalSystem operation for this message
|
|
28
|
+
const operationId = s.messageOperationMap[id];
|
|
29
|
+
if (!operationId) return false;
|
|
30
|
+
|
|
31
|
+
const operation = s.operations[operationId];
|
|
32
|
+
return operation?.type === 'builtinToolLocalSystem' && operation?.status === 'running';
|
|
33
|
+
};
|
|
13
34
|
|
|
14
35
|
export const chatToolSelectors = {
|
|
15
36
|
isDallEImageGenerating,
|
|
@@ -28,6 +28,7 @@ vi.mock('@/services/message', () => ({
|
|
|
28
28
|
createMessage: vi.fn(() => Promise.resolve({ id: 'new-message-id', messages: [] })),
|
|
29
29
|
updateMessage: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
|
|
30
30
|
updateMessageMetadata: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
|
|
31
|
+
updateMessagePlugin: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
|
|
31
32
|
updateMessagePluginError: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
|
|
32
33
|
updateMessageRAG: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
|
|
33
34
|
removeAllMessages: vi.fn(() => Promise.resolve()),
|
|
@@ -688,11 +689,14 @@ describe('chatMessage actions', () => {
|
|
|
688
689
|
await result.current.optimisticUpdateMessageContent(messageId, newContent);
|
|
689
690
|
});
|
|
690
691
|
|
|
691
|
-
expect(internal_dispatchMessageSpy).toHaveBeenCalledWith(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
692
|
+
expect(internal_dispatchMessageSpy).toHaveBeenCalledWith(
|
|
693
|
+
{
|
|
694
|
+
id: messageId,
|
|
695
|
+
type: 'updateMessage',
|
|
696
|
+
value: { content: newContent },
|
|
697
|
+
},
|
|
698
|
+
undefined,
|
|
699
|
+
);
|
|
696
700
|
});
|
|
697
701
|
|
|
698
702
|
it('should replace messages after updating content', async () => {
|
|
@@ -872,10 +876,17 @@ describe('chatMessage actions', () => {
|
|
|
872
876
|
|
|
873
877
|
const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
|
|
874
878
|
|
|
879
|
+
let operationId: string;
|
|
875
880
|
await act(async () => {
|
|
881
|
+
// Create operation with desired context
|
|
882
|
+
const op = result.current.startOperation({
|
|
883
|
+
type: 'sendMessage',
|
|
884
|
+
context: { sessionId: contextSessionId, topicId: contextTopicId },
|
|
885
|
+
});
|
|
886
|
+
operationId = op.operationId;
|
|
887
|
+
|
|
876
888
|
await result.current.optimisticUpdateMessageContent(messageId, content, undefined, {
|
|
877
|
-
|
|
878
|
-
topicId: contextTopicId,
|
|
889
|
+
operationId,
|
|
879
890
|
});
|
|
880
891
|
});
|
|
881
892
|
|
|
@@ -886,7 +897,7 @@ describe('chatMessage actions', () => {
|
|
|
886
897
|
);
|
|
887
898
|
});
|
|
888
899
|
|
|
889
|
-
it('optimisticUpdateMessageError should use context
|
|
900
|
+
it('optimisticUpdateMessageError should use context operationId', async () => {
|
|
890
901
|
const { result } = renderHook(() => useChatStore());
|
|
891
902
|
const messageId = 'message-id';
|
|
892
903
|
const error = { message: 'Error occurred', type: 'error' as any };
|
|
@@ -895,10 +906,17 @@ describe('chatMessage actions', () => {
|
|
|
895
906
|
|
|
896
907
|
const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
|
|
897
908
|
|
|
909
|
+
let operationId: string;
|
|
898
910
|
await act(async () => {
|
|
911
|
+
// Create operation with desired context
|
|
912
|
+
const op = result.current.startOperation({
|
|
913
|
+
type: 'sendMessage',
|
|
914
|
+
context: { sessionId: contextSessionId, topicId: contextTopicId },
|
|
915
|
+
});
|
|
916
|
+
operationId = op.operationId;
|
|
917
|
+
|
|
899
918
|
await result.current.optimisticUpdateMessageError(messageId, error, {
|
|
900
|
-
|
|
901
|
-
topicId: contextTopicId,
|
|
919
|
+
operationId,
|
|
902
920
|
});
|
|
903
921
|
});
|
|
904
922
|
|
|
@@ -909,7 +927,7 @@ describe('chatMessage actions', () => {
|
|
|
909
927
|
);
|
|
910
928
|
});
|
|
911
929
|
|
|
912
|
-
it('optimisticDeleteMessage should use context
|
|
930
|
+
it('optimisticDeleteMessage should use context operationId', async () => {
|
|
913
931
|
const { result } = renderHook(() => useChatStore());
|
|
914
932
|
const messageId = 'message-id';
|
|
915
933
|
const contextSessionId = 'context-session';
|
|
@@ -917,10 +935,17 @@ describe('chatMessage actions', () => {
|
|
|
917
935
|
|
|
918
936
|
const removeMessageSpy = vi.spyOn(messageService, 'removeMessage');
|
|
919
937
|
|
|
938
|
+
let operationId: string;
|
|
920
939
|
await act(async () => {
|
|
940
|
+
// Create operation with desired context
|
|
941
|
+
const op = result.current.startOperation({
|
|
942
|
+
type: 'sendMessage',
|
|
943
|
+
context: { sessionId: contextSessionId, topicId: contextTopicId },
|
|
944
|
+
});
|
|
945
|
+
operationId = op.operationId;
|
|
946
|
+
|
|
921
947
|
await result.current.optimisticDeleteMessage(messageId, {
|
|
922
|
-
|
|
923
|
-
topicId: contextTopicId,
|
|
948
|
+
operationId,
|
|
924
949
|
});
|
|
925
950
|
});
|
|
926
951
|
|
|
@@ -930,7 +955,7 @@ describe('chatMessage actions', () => {
|
|
|
930
955
|
});
|
|
931
956
|
});
|
|
932
957
|
|
|
933
|
-
it('optimisticDeleteMessages should use context
|
|
958
|
+
it('optimisticDeleteMessages should use context operationId', async () => {
|
|
934
959
|
const { result } = renderHook(() => useChatStore());
|
|
935
960
|
const ids = ['id-1', 'id-2'];
|
|
936
961
|
const contextSessionId = 'context-session';
|
|
@@ -938,10 +963,17 @@ describe('chatMessage actions', () => {
|
|
|
938
963
|
|
|
939
964
|
const removeMessagesSpy = vi.spyOn(messageService, 'removeMessages');
|
|
940
965
|
|
|
966
|
+
let operationId: string;
|
|
941
967
|
await act(async () => {
|
|
968
|
+
// Create operation with desired context
|
|
969
|
+
const op = result.current.startOperation({
|
|
970
|
+
type: 'sendMessage',
|
|
971
|
+
context: { sessionId: contextSessionId, topicId: contextTopicId },
|
|
972
|
+
});
|
|
973
|
+
operationId = op.operationId;
|
|
974
|
+
|
|
942
975
|
await result.current.optimisticDeleteMessages(ids, {
|
|
943
|
-
|
|
944
|
-
topicId: contextTopicId,
|
|
976
|
+
operationId,
|
|
945
977
|
});
|
|
946
978
|
});
|
|
947
979
|
|
|
@@ -951,4 +983,90 @@ describe('chatMessage actions', () => {
|
|
|
951
983
|
});
|
|
952
984
|
});
|
|
953
985
|
});
|
|
986
|
+
|
|
987
|
+
describe('optimisticUpdateMessagePlugin', () => {
|
|
988
|
+
it('should dispatch message update action with plugin value', async () => {
|
|
989
|
+
const { result } = renderHook(() => useChatStore());
|
|
990
|
+
const messageId = 'message-id';
|
|
991
|
+
const pluginValue = { arguments: '{"test":"value"}' };
|
|
992
|
+
const internal_dispatchMessageSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
|
|
993
|
+
|
|
994
|
+
await act(async () => {
|
|
995
|
+
await result.current.optimisticUpdateMessagePlugin(messageId, pluginValue);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
expect(internal_dispatchMessageSpy).toHaveBeenCalledWith(
|
|
999
|
+
{
|
|
1000
|
+
id: messageId,
|
|
1001
|
+
type: 'updateMessagePlugin',
|
|
1002
|
+
value: pluginValue,
|
|
1003
|
+
},
|
|
1004
|
+
undefined,
|
|
1005
|
+
);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('should call messageService.updateMessagePlugin with correct parameters', async () => {
|
|
1009
|
+
const { result } = renderHook(() => useChatStore());
|
|
1010
|
+
const messageId = 'message-id';
|
|
1011
|
+
const pluginValue = { state: 'success' };
|
|
1012
|
+
|
|
1013
|
+
const updateMessagePluginSpy = vi.spyOn(messageService, 'updateMessagePlugin');
|
|
1014
|
+
await act(async () => {
|
|
1015
|
+
await result.current.optimisticUpdateMessagePlugin(messageId, pluginValue);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
expect(updateMessagePluginSpy).toHaveBeenCalledWith(messageId, pluginValue, {
|
|
1019
|
+
sessionId: 'session-id',
|
|
1020
|
+
topicId: 'topic-id',
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it('should replace messages after updating plugin', async () => {
|
|
1025
|
+
const { result } = renderHook(() => useChatStore());
|
|
1026
|
+
const messageId = 'message-id';
|
|
1027
|
+
const pluginValue = { apiName: 'test-api' };
|
|
1028
|
+
const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
|
|
1029
|
+
|
|
1030
|
+
await act(async () => {
|
|
1031
|
+
await result.current.optimisticUpdateMessagePlugin(messageId, pluginValue);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
expect(replaceMessagesSpy).toHaveBeenCalledWith(
|
|
1035
|
+
[],
|
|
1036
|
+
expect.objectContaining({
|
|
1037
|
+
sessionId: 'session-id',
|
|
1038
|
+
topicId: 'topic-id',
|
|
1039
|
+
}),
|
|
1040
|
+
);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('should use context operationId when provided', async () => {
|
|
1044
|
+
const { result } = renderHook(() => useChatStore());
|
|
1045
|
+
const messageId = 'message-id';
|
|
1046
|
+
const pluginValue = { identifier: 'test-plugin' };
|
|
1047
|
+
const contextSessionId = 'context-session';
|
|
1048
|
+
const contextTopicId = 'context-topic';
|
|
1049
|
+
|
|
1050
|
+
const updateMessagePluginSpy = vi.spyOn(messageService, 'updateMessagePlugin');
|
|
1051
|
+
|
|
1052
|
+
let operationId: string;
|
|
1053
|
+
await act(async () => {
|
|
1054
|
+
// Create operation with desired context
|
|
1055
|
+
const op = result.current.startOperation({
|
|
1056
|
+
type: 'sendMessage',
|
|
1057
|
+
context: { sessionId: contextSessionId, topicId: contextTopicId },
|
|
1058
|
+
});
|
|
1059
|
+
operationId = op.operationId;
|
|
1060
|
+
|
|
1061
|
+
await result.current.optimisticUpdateMessagePlugin(messageId, pluginValue, {
|
|
1062
|
+
operationId,
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
expect(updateMessagePluginSpy).toHaveBeenCalledWith(messageId, pluginValue, {
|
|
1067
|
+
sessionId: contextSessionId,
|
|
1068
|
+
topicId: contextTopicId,
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
954
1072
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parse } from '@lobechat/conversation-flow';
|
|
2
2
|
import { TraceEventPayloads } from '@lobechat/types';
|
|
3
|
+
import debug from 'debug';
|
|
3
4
|
import isEqual from 'fast-deep-equal';
|
|
4
5
|
import { StateCreator } from 'zustand/vanilla';
|
|
5
6
|
|
|
@@ -10,6 +11,8 @@ import { displayMessageSelectors } from '../../../selectors';
|
|
|
10
11
|
import { messageMapKey } from '../../../utils/messageMapKey';
|
|
11
12
|
import { MessageDispatch, messagesReducer } from '../reducer';
|
|
12
13
|
|
|
14
|
+
const log = debug('lobe-store:message-internals');
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Internal core methods that serve as building blocks for other actions
|
|
15
18
|
*/
|
|
@@ -18,10 +21,7 @@ export interface MessageInternalsAction {
|
|
|
18
21
|
* update message at the frontend
|
|
19
22
|
* this method will not update messages to database
|
|
20
23
|
*/
|
|
21
|
-
internal_dispatchMessage: (
|
|
22
|
-
payload: MessageDispatch,
|
|
23
|
-
context?: { sessionId: string; topicId?: string | null },
|
|
24
|
-
) => void;
|
|
24
|
+
internal_dispatchMessage: (payload: MessageDispatch, context?: { operationId?: string }) => void;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* trace message events for analytics
|
|
@@ -37,10 +37,36 @@ export const messageInternals: StateCreator<
|
|
|
37
37
|
> = (set, get) => ({
|
|
38
38
|
// the internal process method of the AI message
|
|
39
39
|
internal_dispatchMessage: (payload, context) => {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
let sessionId: string;
|
|
41
|
+
let topicId: string | null | undefined;
|
|
42
|
+
|
|
43
|
+
// Get context from operation if operationId is provided
|
|
44
|
+
if (context?.operationId) {
|
|
45
|
+
const operation = get().operations[context.operationId];
|
|
46
|
+
if (!operation) {
|
|
47
|
+
log('[internal_dispatchMessage] ERROR: Operation not found: %s', context.operationId);
|
|
48
|
+
throw new Error(`Operation not found: ${context.operationId}`);
|
|
49
|
+
}
|
|
50
|
+
sessionId = operation.context.sessionId!;
|
|
51
|
+
topicId = operation.context.topicId;
|
|
52
|
+
log(
|
|
53
|
+
'[internal_dispatchMessage] get context from operation %s: sessionId=%s, topicId=%s',
|
|
54
|
+
context.operationId,
|
|
55
|
+
sessionId,
|
|
56
|
+
topicId,
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
// Fallback to global state
|
|
60
|
+
sessionId = get().activeId;
|
|
61
|
+
topicId = get().activeTopicId;
|
|
62
|
+
log(
|
|
63
|
+
'[internal_dispatchMessage] use global context: sessionId=%s, topicId=%s',
|
|
64
|
+
sessionId,
|
|
65
|
+
topicId,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
42
68
|
|
|
43
|
-
const messagesKey = messageMapKey(
|
|
69
|
+
const messagesKey = messageMapKey(sessionId, topicId);
|
|
44
70
|
|
|
45
71
|
// Get raw messages from dbMessagesMap and apply reducer
|
|
46
72
|
const rawMessages = get().dbMessagesMap[messagesKey] || [];
|