@lobehub/chat 0.107.15 → 0.107.16
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 +25 -0
- package/package.json +1 -1
- package/src/app/chat/features/Conversation/ChatList/index.tsx +2 -2
- package/src/store/chat/actions/enhance.ts +2 -2
- package/src/store/chat/actions/message.ts +7 -7
- package/src/store/chat/actions/plugin.ts +2 -2
- package/src/store/chat/actions/topic.test.ts +391 -0
- package/src/store/chat/actions/topic.ts +2 -2
- package/src/store/chat/helpers.test.ts +109 -0
- package/src/store/chat/helpers.ts +10 -3
- package/src/store/chat/selectors/__tests__/chat.test.ts +251 -0
- package/src/store/chat/selectors/__tests__/topic.test.ts +1 -1
- package/src/store/chat/selectors/chat.ts +38 -38
- package/src/store/file/slices/images/action.ts +5 -5
- package/src/store/global/slices/agent.ts +2 -2
- package/src/store/global/slices/common.ts +6 -6
- package/src/store/global/slices/settings.ts +4 -4
- package/src/store/plugin/slices/customPlugin/action.ts +4 -4
- package/src/store/plugin/slices/plugin/action.ts +8 -8
- package/src/store/session/slices/session/action.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.107.16](https://github.com/lobehub/lobe-chat/compare/v0.107.15...v0.107.16)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-12-03**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix custom agent meta issue.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix custom agent meta issue, closes [#567](https://github.com/lobehub/lobe-chat/issues/567) ([75560e1](https://github.com/lobehub/lobe-chat/commit/75560e1))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 0.107.15](https://github.com/lobehub/lobe-chat/compare/v0.107.14...v0.107.15)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2023-12-03**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.107.
|
|
3
|
+
"version": "0.107.16",
|
|
4
4
|
"description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chatbot",
|
|
@@ -18,8 +18,8 @@ import SkeletonList from './SkeletonList';
|
|
|
18
18
|
|
|
19
19
|
const List = memo(() => {
|
|
20
20
|
const { t } = useTranslation('common');
|
|
21
|
-
|
|
22
|
-
const data = useChatStore(chatSelectors.currentChatsWithGuideMessage, isEqual);
|
|
21
|
+
const meta = useSessionStore(agentSelectors.currentAgentMeta, isEqual);
|
|
22
|
+
const data = useChatStore(chatSelectors.currentChatsWithGuideMessage(meta), isEqual);
|
|
23
23
|
|
|
24
24
|
const [init, chatLoadingId, resendMessage, updateMessageContent] = useChatStore((s) => [
|
|
25
25
|
s.messagesInit,
|
|
@@ -10,7 +10,7 @@ import { chatSelectors } from '@/store/chat/selectors';
|
|
|
10
10
|
import { ChatStore } from '@/store/chat/store';
|
|
11
11
|
import { setNamespace } from '@/utils/storeDebug';
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const n = setNamespace('enhance');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* enhance chat action like translate,tts
|
|
@@ -49,7 +49,7 @@ export const chatEnhance: StateCreator<
|
|
|
49
49
|
await messageService.updateMessageTranslate(id, { content: '', from: '', to: targetLang });
|
|
50
50
|
await refreshMessages();
|
|
51
51
|
|
|
52
|
-
toggleChatLoading(true, id,
|
|
52
|
+
toggleChatLoading(true, id, n('translateMessage(start)', { id }) as string);
|
|
53
53
|
|
|
54
54
|
let content = '';
|
|
55
55
|
let from = '';
|
|
@@ -25,7 +25,7 @@ import { setNamespace } from '@/utils/storeDebug';
|
|
|
25
25
|
import { MessageDispatch, messagesReducer } from '../reducers/message';
|
|
26
26
|
import { chatSelectors } from '../selectors';
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const n = setNamespace('message');
|
|
29
29
|
|
|
30
30
|
export interface ChatMessageAction {
|
|
31
31
|
// create
|
|
@@ -191,10 +191,10 @@ export const chatMessage: StateCreator<
|
|
|
191
191
|
|
|
192
192
|
abortController.abort();
|
|
193
193
|
|
|
194
|
-
toggleChatLoading(false, undefined,
|
|
194
|
+
toggleChatLoading(false, undefined, n('stopGenerateMessage') as string);
|
|
195
195
|
},
|
|
196
196
|
updateInputMessage: (message) => {
|
|
197
|
-
set({ inputMessage: message }, false,
|
|
197
|
+
set({ inputMessage: message }, false, n('updateInputMessage', message));
|
|
198
198
|
},
|
|
199
199
|
updateMessageContent: async (id, content) => {
|
|
200
200
|
const { dispatchMessage, refreshMessages } = get();
|
|
@@ -217,7 +217,7 @@ export const chatMessage: StateCreator<
|
|
|
217
217
|
set(
|
|
218
218
|
{ activeId: sessionId, messages, messagesInit: true },
|
|
219
219
|
false,
|
|
220
|
-
|
|
220
|
+
n('useFetchMessages', {
|
|
221
221
|
messages,
|
|
222
222
|
queryKey: key,
|
|
223
223
|
}),
|
|
@@ -291,7 +291,7 @@ export const chatMessage: StateCreator<
|
|
|
291
291
|
|
|
292
292
|
const messages = messagesReducer(get().messages, payload);
|
|
293
293
|
|
|
294
|
-
set({ messages }, false,
|
|
294
|
+
set({ messages }, false, n(`dispatchMessage/${payload.type}`, payload));
|
|
295
295
|
},
|
|
296
296
|
fetchAIChatMessage: async (messages, assistantId) => {
|
|
297
297
|
const { toggleChatLoading, refreshMessages } = get();
|
|
@@ -299,7 +299,7 @@ export const chatMessage: StateCreator<
|
|
|
299
299
|
const abortController = toggleChatLoading(
|
|
300
300
|
true,
|
|
301
301
|
assistantId,
|
|
302
|
-
|
|
302
|
+
n('generateMessage(start)', { assistantId, messages }) as string,
|
|
303
303
|
);
|
|
304
304
|
|
|
305
305
|
const config = getAgentConfig();
|
|
@@ -380,7 +380,7 @@ export const chatMessage: StateCreator<
|
|
|
380
380
|
},
|
|
381
381
|
});
|
|
382
382
|
|
|
383
|
-
toggleChatLoading(false, undefined,
|
|
383
|
+
toggleChatLoading(false, undefined, n('generateMessage(end)') as string);
|
|
384
384
|
|
|
385
385
|
// also exist message like this:
|
|
386
386
|
// 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
|
|
@@ -10,7 +10,7 @@ import { setNamespace } from '@/utils/storeDebug';
|
|
|
10
10
|
|
|
11
11
|
import { chatSelectors } from '../selectors';
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const n = setNamespace('plugin');
|
|
14
14
|
|
|
15
15
|
export interface ChatPluginAction {
|
|
16
16
|
fillPluginMessageContent: (id: string, content: string) => Promise<void>;
|
|
@@ -38,7 +38,7 @@ export const chatPlugin: StateCreator<
|
|
|
38
38
|
const { refreshMessages, coreProcessMessage, toggleChatLoading } = get();
|
|
39
39
|
let data: string;
|
|
40
40
|
try {
|
|
41
|
-
const abortController = toggleChatLoading(true, id,
|
|
41
|
+
const abortController = toggleChatLoading(true, id, n('fetchPlugin') as string);
|
|
42
42
|
data = await chatService.runPluginApi(payload, { signal: abortController?.signal });
|
|
43
43
|
} catch (error) {
|
|
44
44
|
await messageService.updateMessageError(id, error as any);
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { mutate } from 'swr';
|
|
3
|
+
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { LOADING_FLAT } from '@/const/message';
|
|
6
|
+
import { chatService } from '@/services/chat';
|
|
7
|
+
import { messageService } from '@/services/message';
|
|
8
|
+
import { topicService } from '@/services/topic';
|
|
9
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
10
|
+
import { ChatTopic } from '@/types/topic';
|
|
11
|
+
|
|
12
|
+
import { useChatStore } from '../store';
|
|
13
|
+
|
|
14
|
+
// Mock topicService 和 messageService
|
|
15
|
+
vi.mock('@/services/topic', () => ({
|
|
16
|
+
topicService: {
|
|
17
|
+
removeTopics: vi.fn(),
|
|
18
|
+
removeAllTopic: vi.fn(),
|
|
19
|
+
removeTopic: vi.fn(),
|
|
20
|
+
createTopic: vi.fn(),
|
|
21
|
+
updateTitle: vi.fn(),
|
|
22
|
+
updateFavorite: vi.fn(),
|
|
23
|
+
batchRemoveTopics: vi.fn(),
|
|
24
|
+
getTopics: vi.fn(),
|
|
25
|
+
searchTopics: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('@/services/message', () => ({
|
|
30
|
+
messageService: {
|
|
31
|
+
removeMessages: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
// Setup initial state and mocks before each test
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
useChatStore.setState(
|
|
39
|
+
{
|
|
40
|
+
activeId: undefined,
|
|
41
|
+
activeTopicId: undefined,
|
|
42
|
+
// ... initial state
|
|
43
|
+
},
|
|
44
|
+
false,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
// Cleanup mocks after each test
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('topic action', () => {
|
|
54
|
+
describe('openNewTopicOrSaveTopic', () => {
|
|
55
|
+
it('should call switchTopic if activeTopicId exists', async () => {
|
|
56
|
+
const { result } = renderHook(() => useChatStore());
|
|
57
|
+
await act(async () => {
|
|
58
|
+
useChatStore.setState({ activeTopicId: 'existing-topic-id' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
|
|
62
|
+
|
|
63
|
+
await act(async () => {
|
|
64
|
+
result.current.openNewTopicOrSaveTopic();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(switchTopicSpy).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should call saveToTopic if activeTopicId does not exist', async () => {
|
|
71
|
+
const { result } = renderHook(() => useChatStore());
|
|
72
|
+
await act(async () => {
|
|
73
|
+
useChatStore.setState({ activeTopicId: '' });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const saveToTopicSpy = vi.spyOn(result.current, 'saveToTopic');
|
|
77
|
+
|
|
78
|
+
await act(async () => {
|
|
79
|
+
await result.current.openNewTopicOrSaveTopic();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(saveToTopicSpy).toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('saveToTopic', () => {
|
|
86
|
+
it('should not create a topic if there are no messages', async () => {
|
|
87
|
+
const { result } = renderHook(() => useChatStore());
|
|
88
|
+
useChatStore.setState({ messages: [] });
|
|
89
|
+
|
|
90
|
+
const createTopicSpy = vi.spyOn(topicService, 'createTopic');
|
|
91
|
+
|
|
92
|
+
const topicId = await result.current.saveToTopic();
|
|
93
|
+
|
|
94
|
+
expect(createTopicSpy).not.toHaveBeenCalled();
|
|
95
|
+
expect(topicId).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should create a topic and bind messages to it', async () => {
|
|
99
|
+
const { result } = renderHook(() => useChatStore());
|
|
100
|
+
const messages = [{ id: 'message1' }, { id: 'message2' }] as ChatMessage[];
|
|
101
|
+
useChatStore.setState({ messages, activeId: 'session-id' });
|
|
102
|
+
|
|
103
|
+
const createTopicSpy = vi
|
|
104
|
+
.spyOn(topicService, 'createTopic')
|
|
105
|
+
.mockResolvedValue('new-topic-id');
|
|
106
|
+
|
|
107
|
+
const topicId = await result.current.saveToTopic();
|
|
108
|
+
|
|
109
|
+
expect(createTopicSpy).toHaveBeenCalledWith(
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
sessionId: 'session-id',
|
|
112
|
+
messages: messages.map((m) => m.id),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
expect(topicId).toEqual('new-topic-id');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('refreshTopic', () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
vi.mock('swr', async () => {
|
|
121
|
+
const actual = await vi.importActual('swr');
|
|
122
|
+
return {
|
|
123
|
+
...(actual as any),
|
|
124
|
+
mutate: vi.fn(),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
// 在每个测试用例开始前恢复到实际的 SWR 实现
|
|
130
|
+
vi.resetAllMocks();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should call mutate to refresh topics', async () => {
|
|
134
|
+
const { result } = renderHook(() => useChatStore());
|
|
135
|
+
const activeId = 'test-session-id';
|
|
136
|
+
useChatStore.setState({ activeId });
|
|
137
|
+
|
|
138
|
+
// Mock the mutate function to resolve immediately
|
|
139
|
+
|
|
140
|
+
await act(async () => {
|
|
141
|
+
await result.current.refreshTopic();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Check if mutate has been called with the active session ID
|
|
145
|
+
expect(mutate).toHaveBeenCalledWith(activeId);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle errors during refreshing topics', async () => {
|
|
149
|
+
const { result } = renderHook(() => useChatStore());
|
|
150
|
+
const activeId = 'test-session-id';
|
|
151
|
+
useChatStore.setState({ activeId });
|
|
152
|
+
|
|
153
|
+
// Mock the mutate function to throw an error
|
|
154
|
+
// 设置模拟错误
|
|
155
|
+
(mutate as Mock).mockImplementation(() => {
|
|
156
|
+
throw new Error('Mutate error');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await act(async () => {
|
|
160
|
+
await expect(result.current.refreshTopic()).rejects.toThrow('Mutate error');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 确保恢复 mutate 的模拟,以免影响其他测试
|
|
164
|
+
(mutate as Mock).mockReset();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Additional tests for refreshTopic can be added here...
|
|
168
|
+
});
|
|
169
|
+
describe('favoriteTopic', () => {
|
|
170
|
+
it('should update the favorite state of a topic and refresh topics', async () => {
|
|
171
|
+
const { result } = renderHook(() => useChatStore());
|
|
172
|
+
const topicId = 'topic-id';
|
|
173
|
+
const favState = true;
|
|
174
|
+
|
|
175
|
+
const updateFavoriteSpy = vi.spyOn(topicService, 'updateFavorite').mockResolvedValue(true);
|
|
176
|
+
|
|
177
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
178
|
+
|
|
179
|
+
await act(async () => {
|
|
180
|
+
await result.current.favoriteTopic(topicId, favState);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(updateFavoriteSpy).toHaveBeenCalledWith(topicId, favState);
|
|
184
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('useFetchTopics', () => {
|
|
188
|
+
it('should fetch topics for a given session id', async () => {
|
|
189
|
+
const sessionId = 'test-session-id';
|
|
190
|
+
const topics = [{ id: 'topic-id', title: 'Test Topic' }];
|
|
191
|
+
|
|
192
|
+
// Mock the topicService.getTopics to resolve with topics array
|
|
193
|
+
(topicService.getTopics as Mock).mockResolvedValue(topics);
|
|
194
|
+
|
|
195
|
+
// Use the hook with the session id
|
|
196
|
+
const { result } = renderHook(() => useChatStore().useFetchTopics(sessionId));
|
|
197
|
+
|
|
198
|
+
// Wait for the hook to resolve and update the state
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
expect(result.current.data).toEqual(topics);
|
|
201
|
+
});
|
|
202
|
+
expect(useChatStore.getState().topicsInit).toBeTruthy();
|
|
203
|
+
expect(useChatStore.getState().topics).toEqual(topics);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('useSearchTopics', () => {
|
|
207
|
+
it('should search topics with the given keywords', async () => {
|
|
208
|
+
const keywords = 'search-term';
|
|
209
|
+
const searchResults = [{ id: 'searched-topic-id', title: 'Searched Topic' }];
|
|
210
|
+
|
|
211
|
+
// Mock the topicService.searchTopics to resolve with search results
|
|
212
|
+
(topicService.searchTopics as Mock).mockResolvedValue(searchResults);
|
|
213
|
+
|
|
214
|
+
// Use the hook with the keywords
|
|
215
|
+
const { result } = renderHook(() => useChatStore().useSearchTopics(keywords));
|
|
216
|
+
|
|
217
|
+
// Wait for the hook to resolve and update the state
|
|
218
|
+
await waitFor(() => {
|
|
219
|
+
expect(result.current.data).toEqual(searchResults);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe('updateTopicTitle', () => {
|
|
224
|
+
it('should call topicService.updateTitle with correct parameters and refresh the topic', async () => {
|
|
225
|
+
const topicId = 'topic-id';
|
|
226
|
+
const newTitle = 'Updated Topic Title';
|
|
227
|
+
// Mock the topicService.updateTitle to resolve immediately
|
|
228
|
+
(topicService.updateTitle as Mock).mockResolvedValue(undefined);
|
|
229
|
+
|
|
230
|
+
const { result } = renderHook(() => useChatStore());
|
|
231
|
+
|
|
232
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
233
|
+
|
|
234
|
+
// Call the action with the topicId and newTitle
|
|
235
|
+
await act(async () => {
|
|
236
|
+
await result.current.updateTopicTitle(topicId, newTitle);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Verify that the topicService.updateTitle was called with correct parameters
|
|
240
|
+
expect(topicService.updateTitle).toHaveBeenCalledWith(topicId, newTitle);
|
|
241
|
+
|
|
242
|
+
// Verify that the refreshTopic was called to update the state
|
|
243
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('switchTopic', () => {
|
|
247
|
+
it('should update activeTopicId and call refreshMessages', async () => {
|
|
248
|
+
const topicId = 'topic-id';
|
|
249
|
+
const { result } = renderHook(() => useChatStore());
|
|
250
|
+
|
|
251
|
+
const refreshMessagesSpy = vi.spyOn(result.current, 'refreshMessages');
|
|
252
|
+
// Call the switchTopic action with the topicId
|
|
253
|
+
await act(async () => {
|
|
254
|
+
await result.current.switchTopic(topicId);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Verify that the activeTopicId has been updated
|
|
258
|
+
expect(useChatStore.getState().activeTopicId).toBe(topicId);
|
|
259
|
+
|
|
260
|
+
// Verify that the refreshMessages was called to update the messages
|
|
261
|
+
expect(refreshMessagesSpy).toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
describe('removeSessionTopics', () => {
|
|
265
|
+
it('should remove all topics from the current session and refresh the topic list', async () => {
|
|
266
|
+
const { result } = renderHook(() => useChatStore());
|
|
267
|
+
const activeId = 'test-session-id';
|
|
268
|
+
await act(async () => {
|
|
269
|
+
useChatStore.setState({ activeId });
|
|
270
|
+
});
|
|
271
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
272
|
+
const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
|
|
273
|
+
|
|
274
|
+
await act(async () => {
|
|
275
|
+
await result.current.removeSessionTopics();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(topicService.removeTopics).toHaveBeenCalledWith(activeId);
|
|
279
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
280
|
+
expect(switchTopicSpy).toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
describe('removeAllTopics', () => {
|
|
284
|
+
it('should remove all topics and refresh the topic list', async () => {
|
|
285
|
+
const { result } = renderHook(() => useChatStore());
|
|
286
|
+
|
|
287
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
288
|
+
|
|
289
|
+
await act(async () => {
|
|
290
|
+
await result.current.removeAllTopics();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(topicService.removeAllTopic).toHaveBeenCalled();
|
|
294
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
describe('removeTopic', () => {
|
|
298
|
+
it('should remove a specific topic and its messages, then refresh the topic list', async () => {
|
|
299
|
+
const topicId = 'topic-1';
|
|
300
|
+
const { result } = renderHook(() => useChatStore());
|
|
301
|
+
const activeId = 'test-session-id';
|
|
302
|
+
|
|
303
|
+
await act(async () => {
|
|
304
|
+
useChatStore.setState({ activeId });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
308
|
+
const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
|
|
309
|
+
|
|
310
|
+
await act(async () => {
|
|
311
|
+
await result.current.removeTopic(topicId);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(messageService.removeMessages).toHaveBeenCalledWith(activeId, topicId);
|
|
315
|
+
expect(topicService.removeTopic).toHaveBeenCalledWith(topicId);
|
|
316
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
317
|
+
expect(switchTopicSpy).toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
describe('removeUnstarredTopic', () => {
|
|
321
|
+
it('should remove unstarred topics and refresh the topic list', async () => {
|
|
322
|
+
const { result } = renderHook(() => useChatStore());
|
|
323
|
+
// Set up mock state with unstarred topics
|
|
324
|
+
await act(async () => {
|
|
325
|
+
useChatStore.setState({
|
|
326
|
+
topics: [
|
|
327
|
+
{ id: 'topic-1', favorite: false },
|
|
328
|
+
{ id: 'topic-2', favorite: true },
|
|
329
|
+
{ id: 'topic-3', favorite: false },
|
|
330
|
+
] as ChatTopic[],
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
334
|
+
const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
|
|
335
|
+
|
|
336
|
+
await act(async () => {
|
|
337
|
+
await result.current.removeUnstarredTopic();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(topicService.batchRemoveTopics).toHaveBeenCalledWith(['topic-1', 'topic-3']);
|
|
341
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
342
|
+
expect(switchTopicSpy).toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
describe('updateTopicLoading', () => {
|
|
346
|
+
it('should call update topicLoadingId', async () => {
|
|
347
|
+
const { result } = renderHook(() => useChatStore());
|
|
348
|
+
expect(result.current.topicLoadingId).toBeUndefined();
|
|
349
|
+
|
|
350
|
+
// Call the action with the topicId and newTitle
|
|
351
|
+
await act(async () => {
|
|
352
|
+
await result.current.updateTopicLoading('loading-id');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(result.current.topicLoadingId).toEqual('loading-id');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
describe('summaryTopicTitle', () => {
|
|
359
|
+
it('should auto-summarize the topic title and update it', async () => {
|
|
360
|
+
const topicId = 'topic-1';
|
|
361
|
+
const messages = [{ id: 'message-1', content: 'Hello' }] as ChatMessage[];
|
|
362
|
+
const topics = [{ id: 'topic-1', title: 'Test Topic' }] as ChatTopic[];
|
|
363
|
+
const { result } = renderHook(() => useChatStore());
|
|
364
|
+
await act(async () => {
|
|
365
|
+
useChatStore.setState({ topics });
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Mock the `updateTopicTitleInSummary` and `refreshTopic` for spying
|
|
369
|
+
const updateTopicTitleInSummarySpy = vi.spyOn(result.current, 'updateTopicTitleInSummary');
|
|
370
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
371
|
+
|
|
372
|
+
// Mock the `chatService.fetchPresetTaskResult` to simulate the AI response
|
|
373
|
+
vi.spyOn(chatService, 'fetchPresetTaskResult').mockImplementation((params) => {
|
|
374
|
+
if (params) {
|
|
375
|
+
params.onFinish?.('Summarized Title');
|
|
376
|
+
}
|
|
377
|
+
return Promise.resolve(undefined);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await act(async () => {
|
|
381
|
+
await result.current.summaryTopicTitle(topicId, messages);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Verify that the title was updated and the topic was refreshed
|
|
385
|
+
expect(updateTopicTitleInSummarySpy).toHaveBeenCalledWith(topicId, LOADING_FLAT);
|
|
386
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
387
|
+
|
|
388
|
+
// TODO: need to test with fetchPresetTaskResult
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -22,7 +22,7 @@ const n = setNamespace('topic');
|
|
|
22
22
|
|
|
23
23
|
export interface ChatTopicAction {
|
|
24
24
|
favoriteTopic: (id: string, favState: boolean) => Promise<void>;
|
|
25
|
-
openNewTopicOrSaveTopic: () => void
|
|
25
|
+
openNewTopicOrSaveTopic: () => Promise<void>;
|
|
26
26
|
refreshTopic: () => Promise<void>;
|
|
27
27
|
removeAllTopics: () => Promise<void>;
|
|
28
28
|
removeSessionTopics: () => Promise<void>;
|
|
@@ -45,7 +45,7 @@ export const chatTopic: StateCreator<
|
|
|
45
45
|
ChatTopicAction
|
|
46
46
|
> = (set, get) => ({
|
|
47
47
|
// create
|
|
48
|
-
openNewTopicOrSaveTopic: () => {
|
|
48
|
+
openNewTopicOrSaveTopic: async () => {
|
|
49
49
|
const { switchTopic, saveToTopic, activeTopicId } = get();
|
|
50
50
|
const hasTopic = !!activeTopicId;
|
|
51
51
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { LobeAgentConfig } from '@/types/agent';
|
|
4
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
5
|
+
import { OpenAIChatMessage } from '@/types/openai/chat';
|
|
6
|
+
import { encodeAsync } from '@/utils/tokenizer';
|
|
7
|
+
import * as tokenizerObj from '@/utils/tokenizer';
|
|
8
|
+
|
|
9
|
+
import { chatHelpers } from './helpers';
|
|
10
|
+
|
|
11
|
+
// Mock encodeAsync function
|
|
12
|
+
vi.mock('@/utils/tokenizer', () => ({
|
|
13
|
+
encodeAsync: vi.fn((text) => Promise.resolve(text.length)),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('chatHelpers', () => {
|
|
17
|
+
describe('getMessagesTokenCount', () => {
|
|
18
|
+
it('returns token count for a list of messages', async () => {
|
|
19
|
+
const messages = [{ content: 'Hello' }, { content: 'World' }] as OpenAIChatMessage[];
|
|
20
|
+
const tokenCount = await chatHelpers.getMessagesTokenCount(messages);
|
|
21
|
+
expect(tokenCount).toBe('HelloWorld'.length);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns 0 for an empty array', async () => {
|
|
25
|
+
const tokenCount = await chatHelpers.getMessagesTokenCount([]);
|
|
26
|
+
expect(tokenCount).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('handles messages with empty content', async () => {
|
|
30
|
+
const messages = [
|
|
31
|
+
{ content: 'Hello' },
|
|
32
|
+
{ content: '' },
|
|
33
|
+
{ content: 'World' },
|
|
34
|
+
] as OpenAIChatMessage[];
|
|
35
|
+
const tokenCount = await chatHelpers.getMessagesTokenCount(messages);
|
|
36
|
+
expect(tokenCount).toBe('HelloWorld'.length);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('throws an error when encodeAsync fails', async () => {
|
|
40
|
+
vi.spyOn(tokenizerObj, 'encodeAsync').mockRejectedValue(new Error('Test error'));
|
|
41
|
+
await expect(
|
|
42
|
+
chatHelpers.getMessagesTokenCount([{ content: 'Hello' }] as OpenAIChatMessage[]),
|
|
43
|
+
).rejects.toThrow('Test error');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('getMessageById', () => {
|
|
48
|
+
const messages = [
|
|
49
|
+
{ id: '1', content: 'Hello' },
|
|
50
|
+
{ id: '2', content: 'World' },
|
|
51
|
+
] as ChatMessage[];
|
|
52
|
+
|
|
53
|
+
it('finds a message by id', () => {
|
|
54
|
+
const message = chatHelpers.getMessageById(messages, '1');
|
|
55
|
+
expect(message).toEqual({ id: '1', content: 'Hello' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns undefined for an invalid id', () => {
|
|
59
|
+
const message = chatHelpers.getMessageById(messages, '3');
|
|
60
|
+
expect(message).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns undefined for an empty array', () => {
|
|
64
|
+
const message = chatHelpers.getMessageById([], '1');
|
|
65
|
+
expect(message).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('getSlicedMessagesWithConfig', () => {
|
|
70
|
+
const messages = [
|
|
71
|
+
{ id: '1', content: 'First' },
|
|
72
|
+
{ id: '2', content: 'Second' },
|
|
73
|
+
{ id: '3', content: 'Third' },
|
|
74
|
+
] as ChatMessage[];
|
|
75
|
+
|
|
76
|
+
it('returns all messages if history is disabled', () => {
|
|
77
|
+
const config = { enableHistoryCount: false, historyCount: 0 } as LobeAgentConfig;
|
|
78
|
+
const slicedMessages = chatHelpers.getSlicedMessagesWithConfig(messages, config);
|
|
79
|
+
expect(slicedMessages).toEqual(messages);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns last N messages based on historyCount', () => {
|
|
83
|
+
const config = { enableHistoryCount: true, historyCount: 2 } as LobeAgentConfig;
|
|
84
|
+
const slicedMessages = chatHelpers.getSlicedMessagesWithConfig(messages, config);
|
|
85
|
+
expect(slicedMessages).toEqual([
|
|
86
|
+
{ id: '2', content: 'Second' },
|
|
87
|
+
{ id: '3', content: 'Third' },
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns empty array when historyCount is negative', () => {
|
|
92
|
+
const config = { enableHistoryCount: true, historyCount: -1 } as LobeAgentConfig;
|
|
93
|
+
const slicedMessages = chatHelpers.getSlicedMessagesWithConfig(messages, config);
|
|
94
|
+
expect(slicedMessages).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns all messages if historyCount exceeds the array length', () => {
|
|
98
|
+
const config = { enableHistoryCount: true, historyCount: 5 } as LobeAgentConfig;
|
|
99
|
+
const slicedMessages = chatHelpers.getSlicedMessagesWithConfig(messages, config);
|
|
100
|
+
expect(slicedMessages).toEqual(messages);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns an empty array for an empty message array', () => {
|
|
104
|
+
const config = { enableHistoryCount: true, historyCount: 2 } as LobeAgentConfig;
|
|
105
|
+
const slicedMessages = chatHelpers.getSlicedMessagesWithConfig([], config);
|
|
106
|
+
expect(slicedMessages).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -6,18 +6,25 @@ import { encodeAsync } from '@/utils/tokenizer';
|
|
|
6
6
|
export const getMessagesTokenCount = async (messages: OpenAIChatMessage[]) =>
|
|
7
7
|
encodeAsync(messages.map((m) => m.content).join(''));
|
|
8
8
|
|
|
9
|
+
export const getMessageById = (messages: ChatMessage[], id: string) =>
|
|
10
|
+
messages.find((m) => m.id === id);
|
|
11
|
+
|
|
9
12
|
const getSlicedMessagesWithConfig = (
|
|
10
13
|
messages: ChatMessage[],
|
|
11
14
|
config: LobeAgentConfig,
|
|
12
15
|
): ChatMessage[] => {
|
|
13
|
-
//
|
|
16
|
+
// if historyCount is not enabled or set to 0, return all messages
|
|
14
17
|
if (!config.enableHistoryCount || !config.historyCount) return messages;
|
|
15
18
|
|
|
16
|
-
//
|
|
17
|
-
|
|
19
|
+
// if historyCount is negative, return empty array
|
|
20
|
+
if (config.historyCount <= 0) return [];
|
|
21
|
+
|
|
22
|
+
// if historyCount is positive, return last N messages
|
|
23
|
+
return messages.slice(-config.historyCount);
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
export const chatHelpers = {
|
|
27
|
+
getMessageById,
|
|
21
28
|
getMessagesTokenCount,
|
|
22
29
|
getSlicedMessagesWithConfig,
|
|
23
30
|
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
|
|
4
|
+
import { INBOX_SESSION_ID } from '@/const/session';
|
|
5
|
+
import { ChatStore } from '@/store/chat';
|
|
6
|
+
import { initialState } from '@/store/chat/initialState';
|
|
7
|
+
import { useSessionStore } from '@/store/session';
|
|
8
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
9
|
+
import { MetaData } from '@/types/meta';
|
|
10
|
+
import { merge } from '@/utils/merge';
|
|
11
|
+
|
|
12
|
+
import { chatSelectors } from '../chat';
|
|
13
|
+
|
|
14
|
+
vi.mock('i18next', () => ({
|
|
15
|
+
t: vi.fn((key) => key), // Simplified mock return value
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const initialStore = initialState as ChatStore;
|
|
19
|
+
|
|
20
|
+
const mockMessages = [
|
|
21
|
+
{
|
|
22
|
+
id: 'msg1',
|
|
23
|
+
content: 'Hello World',
|
|
24
|
+
role: 'user',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'msg2',
|
|
28
|
+
content: 'Goodbye World',
|
|
29
|
+
role: 'user',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'msg3',
|
|
33
|
+
content: 'Function Message',
|
|
34
|
+
role: 'function',
|
|
35
|
+
plugin: {
|
|
36
|
+
arguments: ['arg1', 'arg2'],
|
|
37
|
+
identifier: 'func1',
|
|
38
|
+
type: 'pluginType',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
] as ChatMessage[];
|
|
42
|
+
|
|
43
|
+
const mockedChats = [
|
|
44
|
+
{
|
|
45
|
+
id: 'msg1',
|
|
46
|
+
content: 'Hello World',
|
|
47
|
+
role: 'user',
|
|
48
|
+
meta: {
|
|
49
|
+
avatar: '😀',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'msg2',
|
|
54
|
+
content: 'Goodbye World',
|
|
55
|
+
role: 'user',
|
|
56
|
+
meta: {
|
|
57
|
+
avatar: '😀',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'msg3',
|
|
62
|
+
content: 'Function Message',
|
|
63
|
+
role: 'function',
|
|
64
|
+
meta: {
|
|
65
|
+
avatar: '🧩',
|
|
66
|
+
title: 'plugin-unknown',
|
|
67
|
+
},
|
|
68
|
+
plugin: {
|
|
69
|
+
arguments: ['arg1', 'arg2'],
|
|
70
|
+
identifier: 'func1',
|
|
71
|
+
type: 'pluginType',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
] as ChatMessage[];
|
|
75
|
+
|
|
76
|
+
const mockChatStore = { messages: mockMessages } as ChatStore;
|
|
77
|
+
|
|
78
|
+
describe('chatSelectors', () => {
|
|
79
|
+
describe('getMessageById', () => {
|
|
80
|
+
it('should return undefined if the message with the given id does not exist', () => {
|
|
81
|
+
const message = chatSelectors.getMessageById('non-existent-id')(initialStore);
|
|
82
|
+
expect(message).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return the message object with the matching id', () => {
|
|
86
|
+
const state = merge(initialStore, { messages: mockMessages });
|
|
87
|
+
const message = chatSelectors.getMessageById('msg1')(state);
|
|
88
|
+
expect(message).toEqual(mockMessages[0]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return the message with the matching id', () => {
|
|
92
|
+
const message = chatSelectors.getMessageById('msg1')(mockChatStore);
|
|
93
|
+
expect(message).toEqual(mockMessages[0]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return undefined if no message matches the id', () => {
|
|
97
|
+
const message = chatSelectors.getMessageById('nonexistent')(mockChatStore);
|
|
98
|
+
expect(message).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('getFunctionMessageProps', () => {
|
|
103
|
+
it('should return the properties of a function message', () => {
|
|
104
|
+
const state = merge(initialStore, {
|
|
105
|
+
messages: mockMessages,
|
|
106
|
+
chatLoadingId: 'msg3', // Assuming this id represents a loading state
|
|
107
|
+
});
|
|
108
|
+
const props = chatSelectors.getFunctionMessageProps(mockMessages[2])(state);
|
|
109
|
+
expect(props).toEqual({
|
|
110
|
+
arguments: ['arg1', 'arg2'],
|
|
111
|
+
command: mockMessages[2].plugin,
|
|
112
|
+
content: 'Function Message',
|
|
113
|
+
id: 'func1',
|
|
114
|
+
loading: true,
|
|
115
|
+
type: 'pluginType',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return loading as false if the message id is not the current loading id', () => {
|
|
120
|
+
const state = merge(initialStore, { messages: mockMessages, chatLoadingId: 'msg1' });
|
|
121
|
+
const props = chatSelectors.getFunctionMessageProps(mockMessages[2])(state);
|
|
122
|
+
expect(props.loading).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return correct properties when no plugin is present', () => {
|
|
126
|
+
const messageWithoutPlugin = {
|
|
127
|
+
id: 'msg4',
|
|
128
|
+
content: 'No Plugin Message',
|
|
129
|
+
role: 'function',
|
|
130
|
+
// No plugin property
|
|
131
|
+
};
|
|
132
|
+
const state = merge(initialStore, {
|
|
133
|
+
messages: [...mockMessages, messageWithoutPlugin],
|
|
134
|
+
chatLoadingId: 'msg1',
|
|
135
|
+
});
|
|
136
|
+
const props = chatSelectors.getFunctionMessageProps(messageWithoutPlugin)(state);
|
|
137
|
+
expect(props).toEqual({
|
|
138
|
+
arguments: undefined,
|
|
139
|
+
command: undefined,
|
|
140
|
+
content: 'No Plugin Message',
|
|
141
|
+
id: undefined,
|
|
142
|
+
loading: false,
|
|
143
|
+
type: undefined,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('currentChatsWithHistoryConfig', () => {
|
|
149
|
+
it('should slice the messages according to the current agent config', () => {
|
|
150
|
+
const state = merge(initialStore, { messages: mockMessages });
|
|
151
|
+
|
|
152
|
+
const chats = chatSelectors.currentChatsWithHistoryConfig(state);
|
|
153
|
+
expect(chats).toHaveLength(3);
|
|
154
|
+
expect(chats).toEqual(mockedChats);
|
|
155
|
+
});
|
|
156
|
+
it('should slice the messages according to config, assuming historyCount is mocked to 2', async () => {
|
|
157
|
+
const state = merge(initialStore, { messages: mockMessages });
|
|
158
|
+
|
|
159
|
+
useSessionStore.getState().updateAgentConfig({ historyCount: 2, enableHistoryCount: true });
|
|
160
|
+
|
|
161
|
+
const chats = chatSelectors.currentChatsWithHistoryConfig(state);
|
|
162
|
+
|
|
163
|
+
expect(chats).toHaveLength(2);
|
|
164
|
+
expect(chats).toEqual([
|
|
165
|
+
{
|
|
166
|
+
id: 'msg2',
|
|
167
|
+
content: 'Goodbye World',
|
|
168
|
+
role: 'user',
|
|
169
|
+
meta: {
|
|
170
|
+
avatar: '😀',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'msg3',
|
|
175
|
+
content: 'Function Message',
|
|
176
|
+
role: 'function',
|
|
177
|
+
meta: {
|
|
178
|
+
avatar: '🧩',
|
|
179
|
+
title: 'plugin-unknown',
|
|
180
|
+
},
|
|
181
|
+
plugin: {
|
|
182
|
+
arguments: ['arg1', 'arg2'],
|
|
183
|
+
identifier: 'func1',
|
|
184
|
+
type: 'pluginType',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('currentChatsWithGuideMessage', () => {
|
|
192
|
+
it('should return existing messages if there are any', () => {
|
|
193
|
+
const state = merge(initialStore, { messages: mockMessages, activeId: 'someActiveId' });
|
|
194
|
+
const chats = chatSelectors.currentChatsWithGuideMessage({} as MetaData)(state);
|
|
195
|
+
expect(chats).toEqual(mockedChats);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should add a guide message if the chat is brand new', () => {
|
|
199
|
+
const state = merge(initialStore, { messages: [], activeId: 'someActiveId' });
|
|
200
|
+
const metaData = { title: 'Mock Agent', description: 'Mock Description' };
|
|
201
|
+
|
|
202
|
+
const chats = chatSelectors.currentChatsWithGuideMessage(metaData)(state);
|
|
203
|
+
|
|
204
|
+
expect(chats).toHaveLength(1);
|
|
205
|
+
expect(chats[0].content).toBeDefined();
|
|
206
|
+
expect(chats[0].meta.avatar).toEqual(DEFAULT_INBOX_AVATAR);
|
|
207
|
+
expect(chats[0].meta).toEqual(expect.objectContaining(metaData));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should use inbox message for INBOX_SESSION_ID', () => {
|
|
211
|
+
const state = merge(initialStore, { messages: [], activeId: INBOX_SESSION_ID });
|
|
212
|
+
const metaData = { title: 'Mock Agent', description: 'Mock Description' };
|
|
213
|
+
|
|
214
|
+
const chats = chatSelectors.currentChatsWithGuideMessage(metaData)(state);
|
|
215
|
+
|
|
216
|
+
expect(chats[0].content).toEqual('inbox.defaultMessage'); // Assuming translation returns a string containing this
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should use agent default message for non-inbox sessions', () => {
|
|
220
|
+
const state = merge(initialStore, { messages: [], activeId: 'someActiveId' });
|
|
221
|
+
const metaData = { title: 'Mock Agent' };
|
|
222
|
+
|
|
223
|
+
const chats = chatSelectors.currentChatsWithGuideMessage(metaData)(state);
|
|
224
|
+
|
|
225
|
+
expect(chats[0].content).toMatch('agentDefaultMessage'); // Assuming translation returns a string containing this
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('chatsMessageString', () => {
|
|
230
|
+
it('should concatenate the contents of all messages returned by currentChatsWithHistoryConfig', () => {
|
|
231
|
+
// Prepare a state with a few messages
|
|
232
|
+
const state = merge(initialStore, {
|
|
233
|
+
messages: mockMessages,
|
|
234
|
+
activeId: 'active-session',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Assume that the currentChatsWithHistoryConfig will return the last two messages
|
|
238
|
+
const expectedString = mockMessages
|
|
239
|
+
.slice(-2)
|
|
240
|
+
.map((m) => m.content)
|
|
241
|
+
.join('');
|
|
242
|
+
|
|
243
|
+
// Call the selector and verify the result
|
|
244
|
+
const concatenatedString = chatSelectors.chatsMessageString(state);
|
|
245
|
+
expect(concatenatedString).toBe(expectedString);
|
|
246
|
+
|
|
247
|
+
// Restore the mocks after the test
|
|
248
|
+
vi.restoreAllMocks();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -3,13 +3,14 @@ import { t } from 'i18next';
|
|
|
3
3
|
|
|
4
4
|
import { DEFAULT_INBOX_AVATAR, DEFAULT_USER_AVATAR } from '@/const/meta';
|
|
5
5
|
import { INBOX_SESSION_ID } from '@/const/session';
|
|
6
|
-
import { chatHelpers } from '@/store/chat/helpers';
|
|
7
6
|
import { useGlobalStore } from '@/store/global';
|
|
8
7
|
import { useSessionStore } from '@/store/session';
|
|
9
8
|
import { agentSelectors } from '@/store/session/selectors';
|
|
10
9
|
import { ChatMessage } from '@/types/chatMessage';
|
|
10
|
+
import { MetaData } from '@/types/meta';
|
|
11
11
|
import { merge } from '@/utils/merge';
|
|
12
12
|
|
|
13
|
+
import { chatHelpers } from '../helpers';
|
|
13
14
|
import type { ChatStore } from '../store';
|
|
14
15
|
|
|
15
16
|
const getMeta = (message: ChatMessage) => {
|
|
@@ -47,40 +48,41 @@ const currentChats = (s: ChatStore): ChatMessage[] => {
|
|
|
47
48
|
|
|
48
49
|
const initTime = Date.now();
|
|
49
50
|
// 针对新助手添加初始化时的自定义消息
|
|
50
|
-
const currentChatsWithGuideMessage =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
51
|
+
const currentChatsWithGuideMessage =
|
|
52
|
+
(meta: MetaData) =>
|
|
53
|
+
(s: ChatStore): ChatMessage[] => {
|
|
54
|
+
const data = currentChats(s);
|
|
55
|
+
|
|
56
|
+
const isBrandNewChat = data.length === 0;
|
|
57
|
+
|
|
58
|
+
if (!isBrandNewChat) return data;
|
|
59
|
+
|
|
60
|
+
const [activeId, isInbox] = [s.activeId, s.activeId === INBOX_SESSION_ID];
|
|
61
|
+
|
|
62
|
+
const inboxMsg = t('inbox.defaultMessage', { ns: 'chat' });
|
|
63
|
+
const agentSystemRoleMsg = t('agentDefaultMessageWithSystemRole', {
|
|
64
|
+
name: meta.title || t('defaultAgent'),
|
|
65
|
+
ns: 'chat',
|
|
66
|
+
systemRole: meta.description,
|
|
67
|
+
});
|
|
68
|
+
const agentMsg = t('agentDefaultMessage', {
|
|
69
|
+
id: activeId,
|
|
70
|
+
name: meta.title || t('defaultAgent'),
|
|
71
|
+
ns: 'chat',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const emptyInboxGuideMessage = {
|
|
75
|
+
content: isInbox ? inboxMsg : !!meta.description ? agentSystemRoleMsg : agentMsg,
|
|
76
|
+
createdAt: initTime,
|
|
77
|
+
extra: {},
|
|
78
|
+
id: 'default',
|
|
79
|
+
meta: merge({ avatar: DEFAULT_INBOX_AVATAR }, meta),
|
|
80
|
+
role: 'assistant',
|
|
81
|
+
updatedAt: initTime,
|
|
82
|
+
} as ChatMessage;
|
|
83
|
+
|
|
84
|
+
return [emptyInboxGuideMessage];
|
|
85
|
+
};
|
|
84
86
|
|
|
85
87
|
const currentChatsWithHistoryConfig = (s: ChatStore): ChatMessage[] => {
|
|
86
88
|
const chats = currentChats(s);
|
|
@@ -105,9 +107,7 @@ const getFunctionMessageProps =
|
|
|
105
107
|
type: plugin?.type as LobePluginType,
|
|
106
108
|
});
|
|
107
109
|
|
|
108
|
-
const getMessageById = (id: string) => (s: ChatStore) =>
|
|
109
|
-
return s.messages.find((m) => m.id === id);
|
|
110
|
-
};
|
|
110
|
+
const getMessageById = (id: string) => (s: ChatStore) => chatHelpers.getMessageById(s.messages, id);
|
|
111
111
|
|
|
112
112
|
export const chatSelectors = {
|
|
113
113
|
chatsMessageString,
|
|
@@ -8,7 +8,7 @@ import { setNamespace } from '@/utils/storeDebug';
|
|
|
8
8
|
|
|
9
9
|
import { FileStore } from '../../store';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const n = setNamespace('image');
|
|
12
12
|
|
|
13
13
|
export interface FileAction {
|
|
14
14
|
clearImageList: () => void;
|
|
@@ -28,7 +28,7 @@ export const createFileSlice: StateCreator<
|
|
|
28
28
|
FileAction
|
|
29
29
|
> = (set, get) => ({
|
|
30
30
|
clearImageList: () => {
|
|
31
|
-
set({ inputFilesList: [] }, false,
|
|
31
|
+
set({ inputFilesList: [] }, false, n('clearImageList'));
|
|
32
32
|
},
|
|
33
33
|
removeAllFiles: async () => {
|
|
34
34
|
await fileService.removeAllFiles();
|
|
@@ -39,7 +39,7 @@ export const createFileSlice: StateCreator<
|
|
|
39
39
|
set(
|
|
40
40
|
({ inputFilesList }) => ({ inputFilesList: inputFilesList.filter((i) => i !== id) }),
|
|
41
41
|
false,
|
|
42
|
-
|
|
42
|
+
n('removeFile'),
|
|
43
43
|
);
|
|
44
44
|
},
|
|
45
45
|
setImageMapItem: (id, item) => {
|
|
@@ -50,7 +50,7 @@ export const createFileSlice: StateCreator<
|
|
|
50
50
|
draft.imagesMap[id] = item;
|
|
51
51
|
}),
|
|
52
52
|
false,
|
|
53
|
-
|
|
53
|
+
n('setImageMapItem'),
|
|
54
54
|
);
|
|
55
55
|
},
|
|
56
56
|
uploadFile: async (file) => {
|
|
@@ -67,7 +67,7 @@ export const createFileSlice: StateCreator<
|
|
|
67
67
|
set(
|
|
68
68
|
({ inputFilesList }) => ({ inputFilesList: [...inputFilesList, data.id] }),
|
|
69
69
|
false,
|
|
70
|
-
|
|
70
|
+
n('uploadFile'),
|
|
71
71
|
);
|
|
72
72
|
} catch (error) {
|
|
73
73
|
// 提示用户上传失败
|
|
@@ -9,7 +9,7 @@ import { setNamespace } from '@/utils/storeDebug';
|
|
|
9
9
|
|
|
10
10
|
import type { GlobalStore } from '../store';
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const n = setNamespace('settings');
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 设置操作
|
|
@@ -29,6 +29,6 @@ export const createAgentSlice: StateCreator<
|
|
|
29
29
|
draft.defaultAgent = merge(draft.defaultAgent, agent);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
set({ settings }, false,
|
|
32
|
+
set({ settings }, false, n('updateDefaultAgent', agent));
|
|
33
33
|
},
|
|
34
34
|
});
|
|
@@ -11,7 +11,7 @@ import { setNamespace } from '@/utils/storeDebug';
|
|
|
11
11
|
import type { GlobalPreference, GlobalState, Guide, SidebarTabKey } from '../initialState';
|
|
12
12
|
import type { GlobalStore } from '../store';
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const n = setNamespace('settings');
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* 设置操作
|
|
@@ -37,25 +37,25 @@ export const createCommonSlice: StateCreator<
|
|
|
37
37
|
CommonAction
|
|
38
38
|
> = (set, get) => ({
|
|
39
39
|
switchSideBar: (key) => {
|
|
40
|
-
set({ sidebarKey: key }, false,
|
|
40
|
+
set({ sidebarKey: key }, false, n('switchSideBar', key));
|
|
41
41
|
},
|
|
42
42
|
toggleChatSideBar: (newValue) => {
|
|
43
43
|
const showChatSideBar =
|
|
44
44
|
typeof newValue === 'boolean' ? newValue : !get().preference.showChatSideBar;
|
|
45
45
|
|
|
46
|
-
get().updatePreference({ showChatSideBar },
|
|
46
|
+
get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue) as string);
|
|
47
47
|
},
|
|
48
48
|
toggleMobileTopic: (newValue) => {
|
|
49
49
|
const mobileShowTopic =
|
|
50
50
|
typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
|
|
51
51
|
|
|
52
|
-
get().updatePreference({ mobileShowTopic },
|
|
52
|
+
get().updatePreference({ mobileShowTopic }, n('toggleMobileTopic', newValue) as string);
|
|
53
53
|
},
|
|
54
54
|
toggleSystemRole: (newValue) => {
|
|
55
55
|
const showSystemRole =
|
|
56
56
|
typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
|
|
57
57
|
|
|
58
|
-
get().updatePreference({ showSystemRole },
|
|
58
|
+
get().updatePreference({ showSystemRole }, n('toggleMobileTopic', newValue) as string);
|
|
59
59
|
},
|
|
60
60
|
updateGuideState: (guide) => {
|
|
61
61
|
const { updatePreference } = get();
|
|
@@ -75,7 +75,7 @@ export const createCommonSlice: StateCreator<
|
|
|
75
75
|
useSWR('checkLatestVersion', featLatestVersion, {
|
|
76
76
|
onSuccess: (data: string) => {
|
|
77
77
|
if (gt(data, CURRENT_VERSION))
|
|
78
|
-
set({ hasNewVersion: true, latestVersion: data }, false,
|
|
78
|
+
set({ hasNewVersion: true, latestVersion: data }, false, n('checkLatestVersion'));
|
|
79
79
|
},
|
|
80
80
|
}),
|
|
81
81
|
});
|
|
@@ -12,7 +12,7 @@ import { setNamespace } from '@/utils/storeDebug';
|
|
|
12
12
|
|
|
13
13
|
import type { GlobalStore } from '../store';
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const n = setNamespace('settings');
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* 设置操作
|
|
@@ -58,10 +58,10 @@ export const createSettingsSlice: StateCreator<
|
|
|
58
58
|
const settings = produce(get().settings, (draft: GlobalSettings) => {
|
|
59
59
|
draft.defaultAgent = DEFAULT_AGENT;
|
|
60
60
|
});
|
|
61
|
-
set({ settings }, false,
|
|
61
|
+
set({ settings }, false, n('resetDefaultAgent'));
|
|
62
62
|
},
|
|
63
63
|
resetSettings: () => {
|
|
64
|
-
set({ settings: DEFAULT_SETTINGS }, false,
|
|
64
|
+
set({ settings: DEFAULT_SETTINGS }, false, n('resetSettings'));
|
|
65
65
|
},
|
|
66
66
|
setOpenAIConfig: (config) => {
|
|
67
67
|
get().setSettings({ languageModel: { openAI: config } });
|
|
@@ -73,7 +73,7 @@ export const createSettingsSlice: StateCreator<
|
|
|
73
73
|
|
|
74
74
|
if (isEqual(prevSetting, nextSettings)) return;
|
|
75
75
|
|
|
76
|
-
set({ settings: merge(prevSetting, settings) }, false,
|
|
76
|
+
set({ settings: merge(prevSetting, settings) }, false, n('setSettings', settings));
|
|
77
77
|
},
|
|
78
78
|
switchSettingTabs: (tab) => {
|
|
79
79
|
set({ settingsTab: tab });
|
|
@@ -8,7 +8,7 @@ import { PluginStore } from '../../store';
|
|
|
8
8
|
import { defaultCustomPlugin } from './initialState';
|
|
9
9
|
import { CustomPluginListDispatch, devPluginListReducer } from './reducers/customPluginList';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const n = setNamespace('customPlugin');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* 代理行为接口
|
|
@@ -40,11 +40,11 @@ export const createCustomPluginSlice: StateCreator<
|
|
|
40
40
|
const { customPluginList } = get();
|
|
41
41
|
|
|
42
42
|
const nextList = devPluginListReducer(customPluginList, payload);
|
|
43
|
-
set({ customPluginList: nextList }, false,
|
|
43
|
+
set({ customPluginList: nextList }, false, n('dispatchCustomPluginList', payload));
|
|
44
44
|
},
|
|
45
45
|
saveToCustomPluginList: (value) => {
|
|
46
46
|
get().dispatchCustomPluginList({ plugin: value, type: 'addItem' });
|
|
47
|
-
set({ newCustomPlugin: defaultCustomPlugin }, false,
|
|
47
|
+
set({ newCustomPlugin: defaultCustomPlugin }, false, n('saveToCustomPluginList'));
|
|
48
48
|
},
|
|
49
49
|
updateCustomPlugin: (id, value) => {
|
|
50
50
|
const { dispatchCustomPluginList, installPlugin } = get();
|
|
@@ -58,7 +58,7 @@ export const createCustomPluginSlice: StateCreator<
|
|
|
58
58
|
set(
|
|
59
59
|
{ newCustomPlugin: merge({}, get().newCustomPlugin, newCustomPlugin) },
|
|
60
60
|
false,
|
|
61
|
-
|
|
61
|
+
n('updateNewDevPlugin'),
|
|
62
62
|
);
|
|
63
63
|
},
|
|
64
64
|
});
|
|
@@ -17,7 +17,7 @@ import { setNamespace } from '@/utils/storeDebug';
|
|
|
17
17
|
import { PluginStore } from '../../store';
|
|
18
18
|
import { PluginDispatch, pluginManifestReducer } from './reducers/manifest';
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const n = setNamespace('plugin');
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* 插件接口
|
|
@@ -73,7 +73,7 @@ export const createPluginSlice: StateCreator<
|
|
|
73
73
|
|
|
74
74
|
await installPlugins(plugins);
|
|
75
75
|
|
|
76
|
-
set({ manifestPrepared: true }, false,
|
|
76
|
+
set({ manifestPrepared: true }, false, n('checkPluginsIsInstalled'));
|
|
77
77
|
},
|
|
78
78
|
deletePluginSettings: (id) => {
|
|
79
79
|
set(
|
|
@@ -81,14 +81,14 @@ export const createPluginSlice: StateCreator<
|
|
|
81
81
|
draft.pluginsSettings[id] = undefined;
|
|
82
82
|
}),
|
|
83
83
|
false,
|
|
84
|
-
|
|
84
|
+
n('deletePluginSettings'),
|
|
85
85
|
);
|
|
86
86
|
},
|
|
87
87
|
dispatchPluginManifest: (payload) => {
|
|
88
88
|
const { pluginManifestMap } = get();
|
|
89
89
|
const nextManifest = pluginManifestReducer(pluginManifestMap, payload);
|
|
90
90
|
|
|
91
|
-
set({ pluginManifestMap: nextManifest }, false,
|
|
91
|
+
set({ pluginManifestMap: nextManifest }, false, n('dispatchPluginManifest', payload));
|
|
92
92
|
},
|
|
93
93
|
installPlugin: async (name) => {
|
|
94
94
|
const plugin = pluginSelectors.getPluginMetaById(name)(get());
|
|
@@ -140,12 +140,12 @@ export const createPluginSlice: StateCreator<
|
|
|
140
140
|
loadPluginStore: async () => {
|
|
141
141
|
const pluginMarketIndex = await pluginService.getPluginList();
|
|
142
142
|
|
|
143
|
-
set({ pluginList: pluginMarketIndex.plugins }, false,
|
|
143
|
+
set({ pluginList: pluginMarketIndex.plugins }, false, n('loadPluginList'));
|
|
144
144
|
|
|
145
145
|
return pluginMarketIndex;
|
|
146
146
|
},
|
|
147
147
|
resetPluginSettings: () => {
|
|
148
|
-
set({ pluginsSettings: {} }, false,
|
|
148
|
+
set({ pluginsSettings: {} }, false, n('resetPluginSettings'));
|
|
149
149
|
},
|
|
150
150
|
updateManifestLoadingState: (key, value) => {
|
|
151
151
|
set(
|
|
@@ -153,7 +153,7 @@ export const createPluginSlice: StateCreator<
|
|
|
153
153
|
draft.pluginManifestLoading[key] = value;
|
|
154
154
|
}),
|
|
155
155
|
false,
|
|
156
|
-
|
|
156
|
+
n('updateManifestLoadingState'),
|
|
157
157
|
);
|
|
158
158
|
},
|
|
159
159
|
updatePluginSettings: (id, settings) => {
|
|
@@ -162,7 +162,7 @@ export const createPluginSlice: StateCreator<
|
|
|
162
162
|
draft.pluginsSettings[id] = merge({}, draft.pluginsSettings[id], settings);
|
|
163
163
|
}),
|
|
164
164
|
false,
|
|
165
|
-
|
|
165
|
+
n('updatePluginSettings'),
|
|
166
166
|
);
|
|
167
167
|
},
|
|
168
168
|
|
|
@@ -18,7 +18,7 @@ import { setNamespace } from '@/utils/storeDebug';
|
|
|
18
18
|
|
|
19
19
|
import { initLobeSession } from './initialState';
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const n = setNamespace('session');
|
|
22
22
|
|
|
23
23
|
const FETCH_SESSIONS_KEY = 'fetchSessions';
|
|
24
24
|
|
|
@@ -80,7 +80,7 @@ export const createSessionSlice: StateCreator<
|
|
|
80
80
|
SessionAction
|
|
81
81
|
> = (set, get) => ({
|
|
82
82
|
activeSession: (sessionId) => {
|
|
83
|
-
set({ activeId: sessionId }, false,
|
|
83
|
+
set({ activeId: sessionId }, false, n('activeSession'));
|
|
84
84
|
},
|
|
85
85
|
|
|
86
86
|
clearSessions: async () => {
|
|
@@ -153,7 +153,7 @@ export const createSessionSlice: StateCreator<
|
|
|
153
153
|
sessions: data,
|
|
154
154
|
},
|
|
155
155
|
false,
|
|
156
|
-
|
|
156
|
+
n('useFetchSessions/onSuccess', data),
|
|
157
157
|
);
|
|
158
158
|
},
|
|
159
159
|
}),
|
|
@@ -161,7 +161,7 @@ export const createSessionSlice: StateCreator<
|
|
|
161
161
|
useSearchSessions: (keyword) =>
|
|
162
162
|
useSWR<LobeSessions>(keyword, sessionService.searchSessions, {
|
|
163
163
|
onSuccess: (data) => {
|
|
164
|
-
set({ searchSessions: data }, false,
|
|
164
|
+
set({ searchSessions: data }, false, n('useSearchSessions(success)', data));
|
|
165
165
|
},
|
|
166
166
|
revalidateOnFocus: false,
|
|
167
167
|
}),
|