@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.
Files changed (52) hide show
  1. package/.github/workflows/desktop-pr-build.yml +2 -2
  2. package/.github/workflows/docker-database.yml +1 -1
  3. package/.github/workflows/docker-pglite.yml +1 -1
  4. package/.github/workflows/docker.yml +1 -1
  5. package/.github/workflows/release-desktop-beta.yml +2 -2
  6. package/CHANGELOG.md +50 -0
  7. package/apps/desktop/package.json +1 -1
  8. package/changelog/v1.json +18 -0
  9. package/docs/development/basic/work-with-server-side-database.mdx +5 -5
  10. package/docs/development/basic/work-with-server-side-database.zh-CN.mdx +5 -5
  11. package/docs/development/tests/integration-testing.zh-CN.mdx +399 -0
  12. package/locales/ar/chat.json +3 -1
  13. package/locales/bg-BG/chat.json +3 -1
  14. package/locales/de-DE/chat.json +3 -1
  15. package/locales/en-US/chat.json +3 -1
  16. package/locales/es-ES/chat.json +3 -1
  17. package/locales/fa-IR/chat.json +3 -1
  18. package/locales/fr-FR/chat.json +3 -1
  19. package/locales/it-IT/chat.json +3 -1
  20. package/locales/ja-JP/chat.json +3 -1
  21. package/locales/ko-KR/chat.json +3 -1
  22. package/locales/nl-NL/chat.json +3 -1
  23. package/locales/pl-PL/chat.json +3 -1
  24. package/locales/pt-BR/chat.json +3 -1
  25. package/locales/ru-RU/chat.json +3 -1
  26. package/locales/tr-TR/chat.json +3 -1
  27. package/locales/vi-VN/chat.json +3 -1
  28. package/locales/zh-CN/chat.json +3 -1
  29. package/locales/zh-TW/chat.json +3 -1
  30. package/package.json +2 -2
  31. package/packages/database/package.json +2 -1
  32. package/packages/database/tests/test-utils.ts +1 -0
  33. package/packages/types/src/message/chat.ts +1 -0
  34. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx +28 -9
  35. package/src/features/DevPanel/index.tsx +7 -1
  36. package/src/features/ElectronTitlebar/UpdateNotification.tsx +19 -2
  37. package/src/locales/default/chat.ts +2 -0
  38. package/src/server/routers/lambda/{agent.test.ts → __tests__/agent.test.ts} +1 -1
  39. package/src/server/routers/lambda/__tests__/aiChat.test.ts +259 -0
  40. package/src/server/routers/lambda/{aiModel.test.ts → __tests__/aiModel.test.ts} +1 -1
  41. package/src/server/routers/lambda/{aiProvider.test.ts → __tests__/aiProvider.test.ts} +1 -1
  42. package/src/server/routers/lambda/{generation.test.ts → __tests__/generation.test.ts} +1 -1
  43. package/src/server/routers/lambda/{generationBatch.test.ts → __tests__/generationBatch.test.ts} +1 -1
  44. package/src/server/routers/lambda/{generationTopic.test.ts → __tests__/generationTopic.test.ts} +1 -1
  45. package/src/server/routers/lambda/__tests__/integration/README.md +110 -0
  46. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +545 -0
  47. package/src/server/routers/lambda/__tests__/integration/setup.ts +36 -0
  48. package/src/server/routers/lambda/{user.test.ts → __tests__/user.test.ts} +1 -1
  49. package/src/server/routers/lambda/aiChat.ts +2 -0
  50. package/src/store/chat/slices/message/action.test.ts +92 -0
  51. package/src/store/chat/slices/message/action.ts +3 -1
  52. 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 } = get();
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
- });