@lobehub/chat 1.139.2 → 1.139.4
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/.github/workflows/desktop-pr-build.yml +2 -2
- package/.github/workflows/docker-database.yml +1 -1
- package/.github/workflows/docker-pglite.yml +1 -1
- package/.github/workflows/docker.yml +1 -1
- package/.github/workflows/release-desktop-beta.yml +2 -2
- package/CHANGELOG.md +50 -0
- package/apps/desktop/package.json +1 -1
- package/changelog/v1.json +18 -0
- package/docs/development/basic/work-with-server-side-database.mdx +5 -5
- package/docs/development/basic/work-with-server-side-database.zh-CN.mdx +5 -5
- package/docs/development/tests/integration-testing.zh-CN.mdx +399 -0
- package/locales/ar/chat.json +3 -1
- package/locales/bg-BG/chat.json +3 -1
- package/locales/de-DE/chat.json +3 -1
- package/locales/en-US/chat.json +3 -1
- package/locales/es-ES/chat.json +3 -1
- package/locales/fa-IR/chat.json +3 -1
- package/locales/fr-FR/chat.json +3 -1
- package/locales/it-IT/chat.json +3 -1
- package/locales/ja-JP/chat.json +3 -1
- package/locales/ko-KR/chat.json +3 -1
- package/locales/nl-NL/chat.json +3 -1
- package/locales/pl-PL/chat.json +3 -1
- package/locales/pt-BR/chat.json +3 -1
- package/locales/ru-RU/chat.json +3 -1
- package/locales/tr-TR/chat.json +3 -1
- package/locales/vi-VN/chat.json +3 -1
- package/locales/zh-CN/chat.json +3 -1
- package/locales/zh-TW/chat.json +3 -1
- package/package.json +2 -2
- package/packages/database/package.json +2 -1
- package/packages/database/tests/test-utils.ts +1 -0
- package/packages/types/src/message/chat.ts +1 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx +28 -9
- package/src/features/DevPanel/index.tsx +7 -1
- package/src/features/ElectronTitlebar/UpdateNotification.tsx +19 -2
- package/src/locales/default/chat.ts +2 -0
- package/src/server/routers/lambda/{agent.test.ts → __tests__/agent.test.ts} +1 -1
- package/src/server/routers/lambda/__tests__/aiChat.test.ts +259 -0
- package/src/server/routers/lambda/{aiModel.test.ts → __tests__/aiModel.test.ts} +1 -1
- package/src/server/routers/lambda/{aiProvider.test.ts → __tests__/aiProvider.test.ts} +1 -1
- package/src/server/routers/lambda/{generation.test.ts → __tests__/generation.test.ts} +1 -1
- package/src/server/routers/lambda/{generationBatch.test.ts → __tests__/generationBatch.test.ts} +1 -1
- package/src/server/routers/lambda/{generationTopic.test.ts → __tests__/generationTopic.test.ts} +1 -1
- package/src/server/routers/lambda/__tests__/integration/README.md +110 -0
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +545 -0
- package/src/server/routers/lambda/__tests__/integration/setup.ts +36 -0
- package/src/server/routers/lambda/{user.test.ts → __tests__/user.test.ts} +1 -1
- package/src/server/routers/lambda/aiChat.ts +2 -0
- package/src/store/chat/slices/message/action.test.ts +92 -0
- package/src/store/chat/slices/message/action.ts +3 -1
- package/src/server/routers/lambda/aiChat.test.ts +0 -108
|
@@ -98,6 +98,7 @@ export const aiChatRouter = router({
|
|
|
98
98
|
files: input.newUserMessage.files,
|
|
99
99
|
role: 'user',
|
|
100
100
|
sessionId: input.sessionId!,
|
|
101
|
+
threadId: input.threadId,
|
|
101
102
|
topicId,
|
|
102
103
|
});
|
|
103
104
|
|
|
@@ -117,6 +118,7 @@ export const aiChatRouter = router({
|
|
|
117
118
|
parentId: messageId,
|
|
118
119
|
role: 'assistant',
|
|
119
120
|
sessionId: input.sessionId!,
|
|
121
|
+
threadId: input.threadId,
|
|
120
122
|
topicId,
|
|
121
123
|
});
|
|
122
124
|
log('assistant message created with id: %s', assistantMessageItem.id);
|
|
@@ -103,6 +103,98 @@ describe('chatMessage actions', () => {
|
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
describe('addUserMessage', () => {
|
|
107
|
+
it('should return early if activeId is undefined', async () => {
|
|
108
|
+
useChatStore.setState({ activeId: undefined });
|
|
109
|
+
const { result } = renderHook(() => useChatStore());
|
|
110
|
+
const updateInputMessageSpy = vi.spyOn(result.current, 'updateInputMessage');
|
|
111
|
+
|
|
112
|
+
await act(async () => {
|
|
113
|
+
await result.current.addUserMessage({ message: 'test message' });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
117
|
+
expect(updateInputMessageSpy).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should call internal_createMessage with correct parameters', async () => {
|
|
121
|
+
const message = 'Test user message';
|
|
122
|
+
const fileList = ['file-id-1', 'file-id-2'];
|
|
123
|
+
useChatStore.setState({
|
|
124
|
+
activeId: mockState.activeId,
|
|
125
|
+
activeTopicId: mockState.activeTopicId,
|
|
126
|
+
});
|
|
127
|
+
const { result } = renderHook(() => useChatStore());
|
|
128
|
+
|
|
129
|
+
await act(async () => {
|
|
130
|
+
await result.current.addUserMessage({ message, fileList });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
134
|
+
content: message,
|
|
135
|
+
files: fileList,
|
|
136
|
+
role: 'user',
|
|
137
|
+
sessionId: mockState.activeId,
|
|
138
|
+
topicId: mockState.activeTopicId,
|
|
139
|
+
threadId: undefined,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should call internal_createMessage with threadId when activeThreadId is set', async () => {
|
|
144
|
+
const message = 'Test user message';
|
|
145
|
+
const activeThreadId = 'thread-123';
|
|
146
|
+
useChatStore.setState({
|
|
147
|
+
activeId: mockState.activeId,
|
|
148
|
+
activeTopicId: mockState.activeTopicId,
|
|
149
|
+
activeThreadId,
|
|
150
|
+
});
|
|
151
|
+
const { result } = renderHook(() => useChatStore());
|
|
152
|
+
|
|
153
|
+
await act(async () => {
|
|
154
|
+
await result.current.addUserMessage({ message });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
158
|
+
content: message,
|
|
159
|
+
files: undefined,
|
|
160
|
+
role: 'user',
|
|
161
|
+
sessionId: mockState.activeId,
|
|
162
|
+
topicId: mockState.activeTopicId,
|
|
163
|
+
threadId: activeThreadId,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should call updateInputMessage with empty string', async () => {
|
|
168
|
+
const { result } = renderHook(() => useChatStore());
|
|
169
|
+
const updateInputMessageSpy = vi.spyOn(result.current, 'updateInputMessage');
|
|
170
|
+
|
|
171
|
+
await act(async () => {
|
|
172
|
+
await result.current.addUserMessage({ message: 'test' });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(updateInputMessageSpy).toHaveBeenCalledWith('');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle message without fileList', async () => {
|
|
179
|
+
const message = 'Test user message without files';
|
|
180
|
+
useChatStore.setState({ activeId: mockState.activeId });
|
|
181
|
+
const { result } = renderHook(() => useChatStore());
|
|
182
|
+
|
|
183
|
+
await act(async () => {
|
|
184
|
+
await result.current.addUserMessage({ message });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
188
|
+
content: message,
|
|
189
|
+
files: undefined,
|
|
190
|
+
role: 'user',
|
|
191
|
+
sessionId: mockState.activeId,
|
|
192
|
+
topicId: mockState.activeTopicId,
|
|
193
|
+
threadId: undefined,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
106
198
|
describe('deleteMessage', () => {
|
|
107
199
|
it('deleteMessage should remove a message by id', async () => {
|
|
108
200
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -255,7 +255,8 @@ export const chatMessage: StateCreator<
|
|
|
255
255
|
updateInputMessage('');
|
|
256
256
|
},
|
|
257
257
|
addUserMessage: async ({ message, fileList }) => {
|
|
258
|
-
const { internal_createMessage, updateInputMessage, activeTopicId, activeId } =
|
|
258
|
+
const { internal_createMessage, updateInputMessage, activeTopicId, activeId, activeThreadId } =
|
|
259
|
+
get();
|
|
259
260
|
if (!activeId) return;
|
|
260
261
|
|
|
261
262
|
await internal_createMessage({
|
|
@@ -265,6 +266,7 @@ export const chatMessage: StateCreator<
|
|
|
265
266
|
sessionId: activeId,
|
|
266
267
|
// if there is activeTopicId,then add topicId to message
|
|
267
268
|
topicId: activeTopicId,
|
|
269
|
+
threadId: activeThreadId,
|
|
268
270
|
});
|
|
269
271
|
|
|
270
272
|
updateInputMessage('');
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { MessageModel } from '@/database/models/message';
|
|
5
|
-
import { TopicModel } from '@/database/models/topic';
|
|
6
|
-
import { AiChatService } from '@/server/services/aiChat';
|
|
7
|
-
|
|
8
|
-
import { aiChatRouter } from './aiChat';
|
|
9
|
-
|
|
10
|
-
vi.mock('@/database/models/message');
|
|
11
|
-
vi.mock('@/database/models/topic');
|
|
12
|
-
vi.mock('@/server/services/aiChat');
|
|
13
|
-
vi.mock('@/server/services/file', () => ({
|
|
14
|
-
FileService: vi.fn(),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
describe('aiChatRouter', () => {
|
|
18
|
-
const mockCtx = { userId: 'u1' };
|
|
19
|
-
|
|
20
|
-
it('should create topic optionally, create user/assistant messages, and return payload', async () => {
|
|
21
|
-
const mockCreateTopic = vi.fn().mockResolvedValue({ id: 't1' });
|
|
22
|
-
const mockCreateMessage = vi
|
|
23
|
-
.fn()
|
|
24
|
-
.mockResolvedValueOnce({ id: 'm-user' })
|
|
25
|
-
.mockResolvedValueOnce({ id: 'm-assistant' });
|
|
26
|
-
const mockGet = vi
|
|
27
|
-
.fn()
|
|
28
|
-
.mockResolvedValue({ messages: [{ id: 'm-user' }, { id: 'm-assistant' }], topics: [{}] });
|
|
29
|
-
|
|
30
|
-
vi.mocked(TopicModel).mockImplementation(() => ({ create: mockCreateTopic }) as any);
|
|
31
|
-
vi.mocked(MessageModel).mockImplementation(() => ({ create: mockCreateMessage }) as any);
|
|
32
|
-
vi.mocked(AiChatService).mockImplementation(() => ({ getMessagesAndTopics: mockGet }) as any);
|
|
33
|
-
|
|
34
|
-
const caller = aiChatRouter.createCaller(mockCtx as any);
|
|
35
|
-
|
|
36
|
-
const input = {
|
|
37
|
-
newAssistantMessage: { model: 'gpt-4o', provider: 'openai' },
|
|
38
|
-
newTopic: { title: 'T', topicMessageIds: ['a', 'b'] },
|
|
39
|
-
newUserMessage: { content: 'hi', files: ['f1'] },
|
|
40
|
-
sessionId: 's1',
|
|
41
|
-
} as any;
|
|
42
|
-
|
|
43
|
-
const res = await caller.sendMessageInServer(input);
|
|
44
|
-
|
|
45
|
-
expect(mockCreateTopic).toHaveBeenCalledWith({
|
|
46
|
-
messages: ['a', 'b'],
|
|
47
|
-
sessionId: 's1',
|
|
48
|
-
title: 'T',
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
expect(mockCreateMessage).toHaveBeenNthCalledWith(1, {
|
|
52
|
-
content: 'hi',
|
|
53
|
-
files: ['f1'],
|
|
54
|
-
role: 'user',
|
|
55
|
-
sessionId: 's1',
|
|
56
|
-
topicId: 't1',
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(mockCreateMessage).toHaveBeenNthCalledWith(
|
|
60
|
-
2,
|
|
61
|
-
expect.objectContaining({
|
|
62
|
-
content: expect.any(String),
|
|
63
|
-
fromModel: 'gpt-4o',
|
|
64
|
-
parentId: 'm-user',
|
|
65
|
-
role: 'assistant',
|
|
66
|
-
sessionId: 's1',
|
|
67
|
-
topicId: 't1',
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
expect(mockGet).toHaveBeenCalledWith({ includeTopic: true, sessionId: 's1', topicId: 't1' });
|
|
72
|
-
expect(res.assistantMessageId).toBe('m-assistant');
|
|
73
|
-
expect(res.userMessageId).toBe('m-user');
|
|
74
|
-
expect(res.isCreateNewTopic).toBe(true);
|
|
75
|
-
expect(res.topicId).toBe('t1');
|
|
76
|
-
expect(res.messages?.length).toBe(2);
|
|
77
|
-
expect(res.topics?.length).toBe(1);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should reuse existing topic when topicId provided', async () => {
|
|
81
|
-
const mockCreateMessage = vi
|
|
82
|
-
.fn()
|
|
83
|
-
.mockResolvedValueOnce({ id: 'm-user' })
|
|
84
|
-
.mockResolvedValueOnce({ id: 'm-assistant' });
|
|
85
|
-
const mockGet = vi.fn().mockResolvedValue({ messages: [], topics: undefined });
|
|
86
|
-
|
|
87
|
-
vi.mocked(MessageModel).mockImplementation(() => ({ create: mockCreateMessage }) as any);
|
|
88
|
-
vi.mocked(AiChatService).mockImplementation(() => ({ getMessagesAndTopics: mockGet }) as any);
|
|
89
|
-
|
|
90
|
-
const caller = aiChatRouter.createCaller(mockCtx as any);
|
|
91
|
-
|
|
92
|
-
const res = await caller.sendMessageInServer({
|
|
93
|
-
newAssistantMessage: { model: 'gpt-4o', provider: 'openai' },
|
|
94
|
-
newUserMessage: { content: 'hi' },
|
|
95
|
-
sessionId: 's1',
|
|
96
|
-
topicId: 't-exist',
|
|
97
|
-
} as any);
|
|
98
|
-
|
|
99
|
-
expect(mockCreateMessage).toHaveBeenCalled();
|
|
100
|
-
expect(mockGet).toHaveBeenCalledWith({
|
|
101
|
-
includeTopic: false,
|
|
102
|
-
sessionId: 's1',
|
|
103
|
-
topicId: 't-exist',
|
|
104
|
-
});
|
|
105
|
-
expect(res.isCreateNewTopic).toBe(false);
|
|
106
|
-
expect(res.topicId).toBe('t-exist');
|
|
107
|
-
});
|
|
108
|
-
});
|