@lobehub/chat 1.122.3 → 1.122.5
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/locales/ar/common.json +7 -0
- package/locales/bg-BG/common.json +7 -0
- package/locales/de-DE/common.json +7 -0
- package/locales/en-US/common.json +7 -0
- package/locales/es-ES/common.json +7 -0
- package/locales/fa-IR/common.json +7 -0
- package/locales/fr-FR/common.json +7 -0
- package/locales/it-IT/common.json +7 -0
- package/locales/ja-JP/common.json +7 -0
- package/locales/ko-KR/common.json +7 -0
- package/locales/nl-NL/common.json +7 -0
- package/locales/pl-PL/common.json +7 -0
- package/locales/pt-BR/common.json +7 -0
- package/locales/ru-RU/common.json +7 -0
- package/locales/tr-TR/common.json +7 -0
- package/locales/vi-VN/common.json +7 -0
- package/locales/zh-TW/common.json +7 -0
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/drizzleMigration.test.ts +70 -0
- package/packages/database/src/models/__tests__/file.test.ts +57 -0
- package/packages/database/src/models/__tests__/session.test.ts +23 -1
- package/packages/database/src/server/models/__tests__/user.test.ts +76 -2
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -1
- package/packages/utils/src/server/auth.ts +2 -2
- package/src/app/(backend)/api/auth/adapter/route.ts +137 -0
- package/src/app/(backend)/api/webhooks/logto/route.ts +9 -0
- package/src/config/auth.ts +4 -0
- package/src/libs/next-auth/adapter/index.ts +103 -201
- package/src/libs/next-auth/auth.config.ts +22 -10
- package/src/libs/next-auth/index.ts +11 -24
- package/src/libs/trpc/edge/context.ts +2 -2
- package/src/libs/trpc/lambda/context.ts +2 -2
- package/src/middleware.ts +2 -2
- package/src/server/routers/lambda/user.test.ts +4 -17
- package/src/server/routers/lambda/user.ts +6 -15
- package/src/server/services/nextAuthUser/index.ts +282 -6
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +437 -0
- package/packages/database/src/server/models/__tests__/nextauth.test.ts +0 -556
- package/src/libs/next-auth/edge.ts +0 -26
- package/src/server/services/nextAuthUser/index.test.ts +0 -108
- /package/src/{libs/next-auth/adapter → server/services/nextAuthUser}/utils.ts +0 -0
@@ -0,0 +1,437 @@
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
2
|
+
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { LOADING_FLAT } from '@/const/message';
|
5
|
+
import {
|
6
|
+
DEFAULT_AGENT_CHAT_CONFIG,
|
7
|
+
DEFAULT_AGENT_CONFIG,
|
8
|
+
DEFAULT_MODEL,
|
9
|
+
DEFAULT_PROVIDER,
|
10
|
+
} from '@/const/settings';
|
11
|
+
import { aiChatService } from '@/services/aiChat';
|
12
|
+
import { chatService } from '@/services/chat';
|
13
|
+
//
|
14
|
+
import { messageService } from '@/services/message';
|
15
|
+
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
|
16
|
+
import { sessionMetaSelectors } from '@/store/session/selectors';
|
17
|
+
import { UploadFileItem } from '@/types/files/upload';
|
18
|
+
import { ChatMessage } from '@/types/message';
|
19
|
+
|
20
|
+
import { useChatStore } from '../../../../store';
|
21
|
+
|
22
|
+
vi.stubGlobal(
|
23
|
+
'fetch',
|
24
|
+
vi.fn(() => Promise.resolve(new Response('mock'))),
|
25
|
+
);
|
26
|
+
|
27
|
+
vi.mock('zustand/traditional');
|
28
|
+
vi.mock('@/const/version', async (importOriginal) => {
|
29
|
+
const module = await importOriginal();
|
30
|
+
return {
|
31
|
+
...(module as any),
|
32
|
+
isServerMode: true,
|
33
|
+
isDesktop: false,
|
34
|
+
};
|
35
|
+
});
|
36
|
+
vi.mock('@/services/aiChat', () => ({
|
37
|
+
aiChatService: {
|
38
|
+
sendMessageInServer: vi.fn(async (params: any) => {
|
39
|
+
const userId = 'user-message-id';
|
40
|
+
const assistantId = 'assistant-message-id';
|
41
|
+
const topicId = params.topicId ?? 'topic-id';
|
42
|
+
return {
|
43
|
+
messages: [
|
44
|
+
{
|
45
|
+
id: userId,
|
46
|
+
role: 'user',
|
47
|
+
content: params.newUserMessage?.content ?? '',
|
48
|
+
sessionId: params.sessionId ?? 'session-id',
|
49
|
+
topicId,
|
50
|
+
} as any,
|
51
|
+
{
|
52
|
+
id: assistantId,
|
53
|
+
role: 'assistant',
|
54
|
+
content: LOADING_FLAT,
|
55
|
+
sessionId: params.sessionId ?? 'session-id',
|
56
|
+
topicId,
|
57
|
+
} as any,
|
58
|
+
],
|
59
|
+
topics: [],
|
60
|
+
topicId,
|
61
|
+
userMessageId: userId,
|
62
|
+
assistantMessageId: assistantId,
|
63
|
+
isCreatNewTopic: !params.topicId,
|
64
|
+
} as any;
|
65
|
+
}),
|
66
|
+
},
|
67
|
+
}));
|
68
|
+
// Mock service
|
69
|
+
vi.mock('@/services/message', () => ({
|
70
|
+
messageService: {
|
71
|
+
getMessages: vi.fn(),
|
72
|
+
updateMessageError: vi.fn(),
|
73
|
+
removeMessage: vi.fn(),
|
74
|
+
removeMessagesByAssistant: vi.fn(),
|
75
|
+
removeMessages: vi.fn(() => Promise.resolve()),
|
76
|
+
createMessage: vi.fn(() => Promise.resolve('new-message-id')),
|
77
|
+
updateMessage: vi.fn(),
|
78
|
+
removeAllMessages: vi.fn(() => Promise.resolve()),
|
79
|
+
},
|
80
|
+
}));
|
81
|
+
vi.mock('@/services/topic', () => ({
|
82
|
+
topicService: {
|
83
|
+
createTopic: vi.fn(() => Promise.resolve()),
|
84
|
+
removeTopic: vi.fn(() => Promise.resolve()),
|
85
|
+
},
|
86
|
+
}));
|
87
|
+
vi.mock('@/services/chat', async (importOriginal) => {
|
88
|
+
const module = await importOriginal();
|
89
|
+
|
90
|
+
return {
|
91
|
+
chatService: {
|
92
|
+
createAssistantMessage: vi.fn(() => Promise.resolve('assistant-message')),
|
93
|
+
createAssistantMessageStream: (module as any).chatService.createAssistantMessageStream,
|
94
|
+
},
|
95
|
+
};
|
96
|
+
});
|
97
|
+
vi.mock('@/services/session', async (importOriginal) => {
|
98
|
+
const module = await importOriginal();
|
99
|
+
|
100
|
+
return {
|
101
|
+
sessionService: {
|
102
|
+
updateSession: vi.fn(),
|
103
|
+
},
|
104
|
+
};
|
105
|
+
});
|
106
|
+
|
107
|
+
const realCoreProcessMessage = useChatStore.getState().internal_execAgentRuntime;
|
108
|
+
|
109
|
+
// Mock state
|
110
|
+
const mockState = {
|
111
|
+
activeId: 'session-id',
|
112
|
+
activeTopicId: 'topic-id',
|
113
|
+
messages: [],
|
114
|
+
refreshMessages: vi.fn(),
|
115
|
+
refreshTopic: vi.fn(),
|
116
|
+
internal_execAgentRuntime: vi.fn(),
|
117
|
+
saveToTopic: vi.fn(),
|
118
|
+
};
|
119
|
+
|
120
|
+
beforeEach(() => {
|
121
|
+
vi.clearAllMocks();
|
122
|
+
useChatStore.setState(mockState, false);
|
123
|
+
vi.spyOn(agentSelectors, 'currentAgentConfig').mockImplementation(() => DEFAULT_AGENT_CONFIG);
|
124
|
+
vi.spyOn(agentChatConfigSelectors, 'currentChatConfig').mockImplementation(
|
125
|
+
() => DEFAULT_AGENT_CHAT_CONFIG,
|
126
|
+
);
|
127
|
+
vi.spyOn(sessionMetaSelectors, 'currentAgentMeta').mockImplementation(() => ({ tags: [] }));
|
128
|
+
});
|
129
|
+
|
130
|
+
afterEach(() => {
|
131
|
+
process.env.NEXT_PUBLIC_BASE_PATH = undefined;
|
132
|
+
|
133
|
+
vi.restoreAllMocks();
|
134
|
+
});
|
135
|
+
|
136
|
+
describe('generateAIChatV2 actions', () => {
|
137
|
+
describe('sendMessageInServer', () => {
|
138
|
+
it('should not send message if there is no active session', async () => {
|
139
|
+
useChatStore.setState({ activeId: undefined });
|
140
|
+
const { result } = renderHook(() => useChatStore());
|
141
|
+
const message = 'Test message';
|
142
|
+
|
143
|
+
await act(async () => {
|
144
|
+
await result.current.sendMessage({ message });
|
145
|
+
});
|
146
|
+
|
147
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
148
|
+
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
149
|
+
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
150
|
+
});
|
151
|
+
|
152
|
+
it('should not send message if message is empty and there are no files', async () => {
|
153
|
+
const { result } = renderHook(() => useChatStore());
|
154
|
+
const message = '';
|
155
|
+
|
156
|
+
await act(async () => {
|
157
|
+
await result.current.sendMessage({ message });
|
158
|
+
});
|
159
|
+
|
160
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
161
|
+
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
162
|
+
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
163
|
+
});
|
164
|
+
|
165
|
+
it('should not send message if message is empty and there are empty files', async () => {
|
166
|
+
const { result } = renderHook(() => useChatStore());
|
167
|
+
const message = '';
|
168
|
+
|
169
|
+
await act(async () => {
|
170
|
+
await result.current.sendMessage({ message, files: [] });
|
171
|
+
});
|
172
|
+
|
173
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
174
|
+
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
175
|
+
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
176
|
+
});
|
177
|
+
|
178
|
+
it('should create message and call internal_execAgentRuntime if message or files are provided', async () => {
|
179
|
+
const { result } = renderHook(() => useChatStore());
|
180
|
+
const message = 'Test message';
|
181
|
+
const files = [{ id: 'file-id' } as UploadFileItem];
|
182
|
+
|
183
|
+
// Mock messageService.create to resolve with a message id
|
184
|
+
(messageService.createMessage as Mock).mockResolvedValue('new-message-id');
|
185
|
+
|
186
|
+
await act(async () => {
|
187
|
+
await result.current.sendMessage({ message, files });
|
188
|
+
});
|
189
|
+
|
190
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith({
|
191
|
+
newAssistantMessage: {
|
192
|
+
model: DEFAULT_MODEL,
|
193
|
+
provider: DEFAULT_PROVIDER,
|
194
|
+
},
|
195
|
+
newUserMessage: {
|
196
|
+
content: message,
|
197
|
+
files: files.map((f) => f.id),
|
198
|
+
},
|
199
|
+
sessionId: mockState.activeId,
|
200
|
+
topicId: mockState.activeTopicId,
|
201
|
+
});
|
202
|
+
expect(result.current.internal_execAgentRuntime).toHaveBeenCalled();
|
203
|
+
});
|
204
|
+
|
205
|
+
it('should handle RAG query when internal_shouldUseRAG returns true', async () => {
|
206
|
+
const { result } = renderHook(() => useChatStore());
|
207
|
+
const message = 'Test RAG query';
|
208
|
+
|
209
|
+
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(true);
|
210
|
+
|
211
|
+
await act(async () => {
|
212
|
+
await result.current.sendMessage({ message });
|
213
|
+
});
|
214
|
+
|
215
|
+
expect(result.current.internal_execAgentRuntime).toHaveBeenCalledWith(
|
216
|
+
expect.objectContaining({
|
217
|
+
ragQuery: message,
|
218
|
+
}),
|
219
|
+
);
|
220
|
+
});
|
221
|
+
|
222
|
+
it('should not use RAG when internal_shouldUseRAG returns false', async () => {
|
223
|
+
const { result } = renderHook(() => useChatStore());
|
224
|
+
const message = 'Test without RAG';
|
225
|
+
|
226
|
+
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(false);
|
227
|
+
vi.spyOn(result.current, 'internal_retrieveChunks');
|
228
|
+
|
229
|
+
await act(async () => {
|
230
|
+
await result.current.sendMessage({ message });
|
231
|
+
});
|
232
|
+
|
233
|
+
expect(result.current.internal_retrieveChunks).not.toHaveBeenCalled();
|
234
|
+
expect(result.current.internal_execAgentRuntime).toHaveBeenCalledWith(
|
235
|
+
expect.objectContaining({
|
236
|
+
ragQuery: undefined,
|
237
|
+
}),
|
238
|
+
);
|
239
|
+
});
|
240
|
+
|
241
|
+
it('should add user message and not call internal_execAgentRuntime if onlyAddUserMessage = true', async () => {
|
242
|
+
const { result } = renderHook(() => useChatStore());
|
243
|
+
|
244
|
+
await act(async () => {
|
245
|
+
await result.current.sendMessage({ message: 'test', onlyAddUserMessage: true });
|
246
|
+
});
|
247
|
+
|
248
|
+
expect(messageService.createMessage).toHaveBeenCalled();
|
249
|
+
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
250
|
+
});
|
251
|
+
|
252
|
+
it('当 isWelcomeQuestion 为 true 时,正确地传递给 internal_execAgentRuntime', async () => {
|
253
|
+
const { result } = renderHook(() => useChatStore());
|
254
|
+
|
255
|
+
await act(async () => {
|
256
|
+
await result.current.sendMessage({ message: 'test', isWelcomeQuestion: true });
|
257
|
+
});
|
258
|
+
|
259
|
+
expect(result.current.internal_execAgentRuntime).toHaveBeenCalledWith(
|
260
|
+
expect.objectContaining({
|
261
|
+
isWelcomeQuestion: true,
|
262
|
+
}),
|
263
|
+
);
|
264
|
+
});
|
265
|
+
|
266
|
+
it('当只有文件而没有消息内容时,正确发送消息', async () => {
|
267
|
+
const { result } = renderHook(() => useChatStore());
|
268
|
+
|
269
|
+
await act(async () => {
|
270
|
+
await result.current.sendMessage({ message: '', files: [{ id: 'file-1' }] as any });
|
271
|
+
});
|
272
|
+
|
273
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith({
|
274
|
+
newAssistantMessage: {
|
275
|
+
model: DEFAULT_MODEL,
|
276
|
+
provider: DEFAULT_PROVIDER,
|
277
|
+
},
|
278
|
+
newUserMessage: {
|
279
|
+
content: '',
|
280
|
+
files: ['file-1'],
|
281
|
+
},
|
282
|
+
sessionId: 'session-id',
|
283
|
+
topicId: 'topic-id',
|
284
|
+
});
|
285
|
+
});
|
286
|
+
|
287
|
+
it('当同时有文件和消息内容时,正确发送消息并关联文件', async () => {
|
288
|
+
const { result } = renderHook(() => useChatStore());
|
289
|
+
|
290
|
+
await act(async () => {
|
291
|
+
await result.current.sendMessage({ message: 'test', files: [{ id: 'file-1' }] as any });
|
292
|
+
});
|
293
|
+
|
294
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith({
|
295
|
+
newAssistantMessage: {
|
296
|
+
model: DEFAULT_MODEL,
|
297
|
+
provider: DEFAULT_PROVIDER,
|
298
|
+
},
|
299
|
+
newUserMessage: {
|
300
|
+
content: 'test',
|
301
|
+
files: ['file-1'],
|
302
|
+
},
|
303
|
+
sessionId: 'session-id',
|
304
|
+
topicId: 'topic-id',
|
305
|
+
});
|
306
|
+
});
|
307
|
+
|
308
|
+
it('当 createMessage 抛出错误时,正确处理错误而不影响整个应用', async () => {
|
309
|
+
const { result } = renderHook(() => useChatStore());
|
310
|
+
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(
|
311
|
+
new Error('create message error'),
|
312
|
+
);
|
313
|
+
|
314
|
+
try {
|
315
|
+
await result.current.sendMessage({ message: 'test' });
|
316
|
+
} catch (e) {}
|
317
|
+
|
318
|
+
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
319
|
+
});
|
320
|
+
|
321
|
+
// it('自动创建主题成功后,正确地将消息复制到新主题,并删除之前的临时消息', async () => {
|
322
|
+
// const { result } = renderHook(() => useChatStore());
|
323
|
+
// act(() => {
|
324
|
+
// useAgentStore.setState({
|
325
|
+
// agentConfig: { enableAutoCreateTopic: true, autoCreateTopicThreshold: 1 },
|
326
|
+
// });
|
327
|
+
//
|
328
|
+
// useChatStore.setState({
|
329
|
+
// // Mock the currentChats selector to return a list that does not reach the threshold
|
330
|
+
// messagesMap: {
|
331
|
+
// [messageMapKey('inbox')]: [{ id: '1' }, { id: '2' }] as ChatMessage[],
|
332
|
+
// },
|
333
|
+
// activeId: 'inbox',
|
334
|
+
// });
|
335
|
+
// });
|
336
|
+
// vi.spyOn(topicService, 'createTopic').mockResolvedValue('new-topic');
|
337
|
+
//
|
338
|
+
// await act(async () => {
|
339
|
+
// await result.current.sendMessage({ message: 'test' });
|
340
|
+
// });
|
341
|
+
//
|
342
|
+
// expect(result.current.messagesMap[messageMapKey('inbox')]).toEqual([
|
343
|
+
// // { id: '1' },
|
344
|
+
// // { id: '2' },
|
345
|
+
// // { id: 'temp-id', content: 'test', role: 'user' },
|
346
|
+
// ]);
|
347
|
+
// // expect(result.current.getMessages('session-id')).toEqual([]);
|
348
|
+
// });
|
349
|
+
|
350
|
+
// it('自动创建主题失败时,正确地处理错误,不会影响后续的消息发送', async () => {
|
351
|
+
// const { result } = renderHook(() => useChatStore());
|
352
|
+
// result.current.setAgentConfig({ enableAutoCreateTopic: true, autoCreateTopicThreshold: 1 });
|
353
|
+
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any);
|
354
|
+
// vi.spyOn(topicService, 'createTopic').mockRejectedValue(new Error('create topic error'));
|
355
|
+
//
|
356
|
+
// await act(async () => {
|
357
|
+
// await result.current.sendMessage({ message: 'test' });
|
358
|
+
// });
|
359
|
+
//
|
360
|
+
// expect(result.current.getMessages('session-id')).toEqual([
|
361
|
+
// { id: '1' },
|
362
|
+
// { id: '2' },
|
363
|
+
// { id: 'new-message-id', content: 'test', role: 'user' },
|
364
|
+
// ]);
|
365
|
+
// });
|
366
|
+
|
367
|
+
// it('当 activeTopicId 不存在且 autoCreateTopic 为 true,但消息数量未达到阈值时,正确地总结主题标题', async () => {
|
368
|
+
// const { result } = renderHook(() => useChatStore());
|
369
|
+
// result.current.setAgentConfig({ enableAutoCreateTopic: true, autoCreateTopicThreshold: 10 });
|
370
|
+
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any);
|
371
|
+
// result.current.setActiveTopic({ id: 'topic-1', title: '' });
|
372
|
+
//
|
373
|
+
// await act(async () => {
|
374
|
+
// await result.current.sendMessage({ message: 'test' });
|
375
|
+
// });
|
376
|
+
//
|
377
|
+
// expect(result.current.summaryTopicTitle).toHaveBeenCalledWith('topic-1', [
|
378
|
+
// { id: '1' },
|
379
|
+
// { id: '2' },
|
380
|
+
// { id: 'new-message-id', content: 'test', role: 'user' },
|
381
|
+
// { id: 'assistant-message', role: 'assistant' },
|
382
|
+
// ]);
|
383
|
+
// });
|
384
|
+
//
|
385
|
+
// it('当 activeTopicId 存在且主题标题为空时,正确地总结主题标题', async () => {
|
386
|
+
// const { result } = renderHook(() => useChatStore());
|
387
|
+
// result.current.setActiveTopic({ id: 'topic-1', title: '' });
|
388
|
+
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any, 'session-id', 'topic-1');
|
389
|
+
//
|
390
|
+
// await act(async () => {
|
391
|
+
// await result.current.sendMessage({ message: 'test' });
|
392
|
+
// });
|
393
|
+
//
|
394
|
+
// expect(result.current.summaryTopicTitle).toHaveBeenCalledWith('topic-1', [
|
395
|
+
// { id: '1' },
|
396
|
+
// { id: '2' },
|
397
|
+
// { id: 'new-message-id', content: 'test', role: 'user' },
|
398
|
+
// { id: 'assistant-message', role: 'assistant' },
|
399
|
+
// ]);
|
400
|
+
// });
|
401
|
+
});
|
402
|
+
|
403
|
+
describe('internal_execAgentRuntime', () => {
|
404
|
+
it('should handle the core AI message processing', async () => {
|
405
|
+
useChatStore.setState({ internal_execAgentRuntime: realCoreProcessMessage });
|
406
|
+
|
407
|
+
const { result } = renderHook(() => useChatStore());
|
408
|
+
const userMessage = {
|
409
|
+
id: 'user-message-id',
|
410
|
+
role: 'user',
|
411
|
+
content: 'Hello, world!',
|
412
|
+
sessionId: mockState.activeId,
|
413
|
+
topicId: mockState.activeTopicId,
|
414
|
+
} as ChatMessage;
|
415
|
+
const messages = [userMessage];
|
416
|
+
|
417
|
+
// 模拟 AI 响应
|
418
|
+
const aiResponse = 'Hello, human!';
|
419
|
+
(chatService.createAssistantMessage as Mock).mockResolvedValue(aiResponse);
|
420
|
+
const spy = vi.spyOn(chatService, 'createAssistantMessageStream');
|
421
|
+
|
422
|
+
await act(async () => {
|
423
|
+
await result.current.internal_execAgentRuntime({
|
424
|
+
messages,
|
425
|
+
userMessageId: userMessage.id,
|
426
|
+
assistantMessageId: 'abc',
|
427
|
+
});
|
428
|
+
});
|
429
|
+
|
430
|
+
// 验证 AI 服务是否被调用
|
431
|
+
expect(spy).toHaveBeenCalled();
|
432
|
+
|
433
|
+
// 验证消息列表是否刷新
|
434
|
+
expect(mockState.refreshMessages).toHaveBeenCalled();
|
435
|
+
});
|
436
|
+
});
|
437
|
+
});
|