@lobehub/chat 0.106.0 → 0.107.0

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 (214) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/locales/en_US/common.json +19 -0
  3. package/locales/en_US/migration.json +45 -0
  4. package/locales/en_US/setting.json +7 -7
  5. package/locales/ja_JP/common.json +19 -0
  6. package/locales/ja_JP/migration.json +45 -0
  7. package/locales/ja_JP/setting.json +2 -2
  8. package/locales/ko_KR/common.json +19 -0
  9. package/locales/ko_KR/migration.json +45 -0
  10. package/locales/ko_KR/setting.json +2 -2
  11. package/locales/ru_RU/common.json +19 -0
  12. package/locales/ru_RU/migration.json +45 -0
  13. package/locales/ru_RU/setting.json +2 -2
  14. package/locales/zh_CN/common.json +19 -0
  15. package/locales/zh_CN/migration.json +45 -0
  16. package/locales/zh_CN/setting.json +1 -1
  17. package/locales/zh_TW/common.json +19 -0
  18. package/locales/zh_TW/migration.json +45 -0
  19. package/locales/zh_TW/setting.json +2 -2
  20. package/package.json +2 -1
  21. package/src/app/api/openai/chat/createChatCompletion.test.ts +4 -4
  22. package/src/app/api/openai/chat/createChatCompletion.ts +4 -10
  23. package/src/app/api/openai/createBizOpenAI/index.ts +1 -1
  24. package/src/app/api/openai/errorResponse.test.ts +1 -1
  25. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +2 -1
  26. package/src/app/chat/(mobile)/features/ChatInput/SendButton.tsx +2 -2
  27. package/src/app/chat/features/ChatHeader/ShareButton/Inner.tsx +2 -5
  28. package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +2 -2
  29. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +2 -2
  30. package/src/app/chat/features/ChatInput/ActionBar/Token/TokenTag.tsx +8 -2
  31. package/src/app/chat/features/ChatInput/InputAreaInner/index.tsx +2 -2
  32. package/src/app/chat/features/ChatInput/STT/index.tsx +2 -2
  33. package/src/app/chat/features/ChatInput/Topic/index.tsx +2 -2
  34. package/src/app/chat/features/ChatInput/useSend.ts +3 -3
  35. package/src/app/chat/features/Conversation/ChatList/Actions/index.ts +2 -2
  36. package/src/app/chat/features/Conversation/ChatList/Error/ApiKeyForm.tsx +2 -2
  37. package/src/app/chat/features/Conversation/ChatList/Error/InvalidAccess.tsx +2 -2
  38. package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +2 -2
  39. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.test.tsx +4 -4
  40. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +30 -31
  41. package/src/app/chat/features/Conversation/ChatList/Extras/TTS.tsx +2 -2
  42. package/src/app/chat/features/Conversation/ChatList/Extras/Translate.tsx +2 -2
  43. package/src/app/chat/features/Conversation/ChatList/Extras/User.tsx +8 -5
  44. package/src/app/chat/features/Conversation/ChatList/Extras/index.ts +3 -3
  45. package/src/app/chat/features/Conversation/ChatList/Extras/type.ts +5 -0
  46. package/src/app/chat/features/Conversation/ChatList/Messages/Assistant.tsx +10 -6
  47. package/src/app/chat/features/Conversation/ChatList/Messages/Default.tsx +7 -3
  48. package/src/app/chat/features/Conversation/ChatList/Messages/Function.tsx +7 -6
  49. package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +7 -7
  50. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +6 -6
  51. package/src/app/chat/features/Conversation/ChatList/index.tsx +16 -24
  52. package/src/app/chat/features/Conversation/ScrollAnchor/index.tsx +2 -2
  53. package/src/app/chat/features/Conversation/index.tsx +3 -3
  54. package/src/app/chat/features/Conversation/useInitConversation.ts +43 -0
  55. package/src/app/chat/features/Migration/ExportConfigButton.tsx +33 -0
  56. package/src/app/chat/features/Migration/Failed.tsx +115 -0
  57. package/src/app/chat/features/Migration/Modal.tsx +80 -0
  58. package/src/app/chat/features/Migration/Start.tsx +106 -0
  59. package/src/app/chat/features/Migration/UpgradeButton.tsx +74 -0
  60. package/src/app/chat/features/Migration/const.ts +15 -0
  61. package/src/app/chat/features/Migration/index.tsx +44 -0
  62. package/src/app/chat/features/SessionListContent/DefaultMode.tsx +54 -0
  63. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +5 -4
  64. package/src/app/chat/features/SessionListContent/List/Item/index.tsx +22 -28
  65. package/src/app/chat/features/SessionListContent/SearchMode.tsx +15 -0
  66. package/src/app/chat/features/SessionListContent/index.tsx +4 -44
  67. package/src/app/chat/features/SessionSearchBar/index.tsx +11 -3
  68. package/src/app/chat/features/TopicListContent/Header.tsx +4 -4
  69. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +11 -9
  70. package/src/app/chat/features/TopicListContent/Topic/TopicItem.tsx +15 -13
  71. package/src/app/chat/features/TopicListContent/Topic/index.tsx +9 -16
  72. package/src/app/chat/features/TopicListContent/TopicSearchBar/index.tsx +13 -4
  73. package/src/app/chat/features/TopicListContent/index.tsx +1 -2
  74. package/src/app/chat/page.tsx +6 -1
  75. package/src/app/chat/settings/features/HeaderContent.tsx +3 -3
  76. package/src/app/home/Redirect.tsx +13 -7
  77. package/src/app/settings/(mobile)/features/ExtraList.tsx +12 -4
  78. package/src/app/settings/common/Common.tsx +16 -4
  79. package/src/app/welcome/features/Banner/index.tsx +9 -10
  80. package/src/chains/summaryTitle.ts +5 -2
  81. package/src/components/DataStyleModal/index.tsx +64 -0
  82. package/src/config/client.ts +5 -0
  83. package/src/const/session.ts +21 -0
  84. package/src/const/settings.ts +1 -1
  85. package/src/database/core/__tests__/db.test.ts +8 -5
  86. package/src/database/core/__tests__/model.test.ts +3 -3
  87. package/src/database/core/db.ts +38 -10
  88. package/src/database/core/index.ts +1 -0
  89. package/src/database/core/model.ts +121 -10
  90. package/src/database/core/schemas.ts +22 -0
  91. package/src/database/core/types/db.ts +13 -0
  92. package/src/database/models/__DEBUG.ts +119 -0
  93. package/src/database/models/{file.test.ts → __tests__/file.test.ts} +19 -6
  94. package/src/database/models/__tests__/message.test.ts +314 -0
  95. package/src/database/models/__tests__/session.test.ts +117 -0
  96. package/src/database/models/__tests__/topic.test.ts +250 -0
  97. package/src/database/models/file.ts +9 -5
  98. package/src/database/models/message.ts +186 -0
  99. package/src/database/models/session.ts +160 -0
  100. package/src/database/models/topic.ts +196 -0
  101. package/src/{types/database → database/schemas}/files.ts +2 -2
  102. package/src/database/schemas/message.ts +37 -0
  103. package/src/database/schemas/session.ts +56 -0
  104. package/src/database/schemas/topic.ts +12 -0
  105. package/src/features/AgentSetting/AgentTTS/SelectWithTTSPreview.tsx +1 -1
  106. package/src/features/AgentSetting/store/action.ts +1 -1
  107. package/src/features/AgentSetting/store/initialState.ts +1 -1
  108. package/src/features/AgentSetting/store/reducers/config.ts +1 -1
  109. package/src/features/DataImporter/index.tsx +240 -0
  110. package/src/features/DebugUI/index.tsx +34 -0
  111. package/src/features/SideBar/BottomActions.tsx +8 -14
  112. package/src/hooks/useImportConfig.ts +13 -30
  113. package/src/hooks/useTTS.ts +1 -1
  114. package/src/layout/GlobalLayout/index.tsx +16 -1
  115. package/src/locales/default/common.ts +19 -0
  116. package/src/locales/default/migration.ts +47 -0
  117. package/src/locales/default/setting.ts +1 -1
  118. package/src/locales/resources/en_US.ts +2 -0
  119. package/src/locales/resources/ja_JP.ts +2 -0
  120. package/src/locales/resources/ko_KR.ts +2 -0
  121. package/src/locales/resources/ru_RU.ts +2 -0
  122. package/src/locales/resources/zh_CN.ts +2 -0
  123. package/src/locales/resources/zh_TW.ts +2 -0
  124. package/src/migrations/FromV0ToV1.ts +2 -4
  125. package/src/migrations/FromV1ToV2/fixtures/input-v1-session.json +191 -0
  126. package/src/migrations/FromV1ToV2/fixtures/output-v2.json +202 -0
  127. package/src/migrations/FromV1ToV2/index.ts +82 -0
  128. package/src/migrations/FromV1ToV2/migrations.test.ts +224 -0
  129. package/src/migrations/FromV1ToV2/types/v1.ts +78 -0
  130. package/src/migrations/FromV1ToV2/types/v2.ts +52 -0
  131. package/src/{utils → migrations}/VersionController.test.ts +1 -3
  132. package/src/{utils → migrations}/VersionController.ts +5 -2
  133. package/src/migrations/index.ts +17 -3
  134. package/src/services/__tests__/file.test.ts +3 -3
  135. package/src/services/chat.ts +66 -5
  136. package/src/services/config.ts +184 -0
  137. package/src/services/debug.ts +13 -0
  138. package/src/services/file.ts +6 -2
  139. package/src/services/github.ts +25 -0
  140. package/src/services/message.ts +79 -0
  141. package/src/services/session.ts +63 -0
  142. package/src/services/topic.ts +56 -0
  143. package/src/store/chat/actions/enhance.ts +95 -0
  144. package/src/store/{session/slices/chat → chat}/actions/message.ts +225 -247
  145. package/src/store/{session/slices/chat → chat}/actions/plugin.ts +29 -34
  146. package/src/store/{session/slices/chat → chat}/actions/share.ts +9 -13
  147. package/src/store/chat/actions/topic.ts +197 -0
  148. package/src/store/chat/helpers.ts +23 -0
  149. package/src/store/chat/index.ts +3 -0
  150. package/src/store/chat/initialState.ts +48 -0
  151. package/src/store/chat/reducers/message.test.ts +83 -0
  152. package/src/store/chat/reducers/message.ts +83 -0
  153. package/src/store/chat/selectors/__tests__/topic.test.ts +43 -0
  154. package/src/store/chat/selectors/chat.ts +119 -0
  155. package/src/store/chat/selectors/index.ts +2 -0
  156. package/src/store/chat/selectors/topic.ts +28 -0
  157. package/src/store/chat/store.ts +63 -0
  158. package/src/store/files/slices/images/action.ts +6 -4
  159. package/src/store/global/store.ts +1 -1
  160. package/src/store/middleware/createHyperStorage/index.ts +40 -24
  161. package/src/store/middleware/createHyperStorage/keyMapper.ts +8 -2
  162. package/src/store/middleware/createHyperStorage/type.ts +10 -8
  163. package/src/store/plugin/slices/plugin/action.ts +1 -1
  164. package/src/store/plugin/store.ts +2 -4
  165. package/src/store/session/helpers.ts +1 -1
  166. package/src/store/session/hooks/useSessionHydrated.ts +6 -10
  167. package/src/store/session/initialState.ts +26 -7
  168. package/src/store/session/selectors.ts +1 -2
  169. package/src/store/session/slices/agent/action.ts +26 -35
  170. package/src/store/session/slices/agent/selectors.ts +32 -37
  171. package/src/store/session/slices/session/action.ts +81 -88
  172. package/src/store/session/slices/session/helpers.ts +19 -0
  173. package/src/store/session/slices/session/initialState.ts +7 -10
  174. package/src/store/session/slices/session/selectors/export.ts +12 -9
  175. package/src/store/session/slices/session/selectors/index.ts +2 -35
  176. package/src/store/session/slices/session/selectors/list.test.ts +25 -203
  177. package/src/store/session/slices/session/selectors/list.ts +40 -78
  178. package/src/store/session/store.ts +11 -17
  179. package/src/types/agent/index.ts +61 -0
  180. package/src/types/chatMessage.ts +11 -20
  181. package/src/types/exportConfig.ts +7 -2
  182. package/src/types/files.ts +2 -2
  183. package/src/types/llm.ts +1 -1
  184. package/src/types/meta.ts +24 -12
  185. package/src/types/openai/chat.ts +9 -1
  186. package/src/types/openai/functionCall.ts +4 -0
  187. package/src/types/session.ts +10 -94
  188. package/src/types/translate.ts +2 -1
  189. package/src/utils/config.ts +5 -11
  190. package/src/utils/fetch.ts +8 -2
  191. package/src/app/chat/features/Conversation/usePluginsInit.ts +0 -14
  192. package/src/database/core/schema.ts +0 -9
  193. package/src/helpers/export.ts +0 -69
  194. package/src/hooks/useExportConfig.ts +0 -6
  195. package/src/store/session/slices/chat/actions/index.ts +0 -32
  196. package/src/store/session/slices/chat/actions/topic.ts +0 -171
  197. package/src/store/session/slices/chat/actions/translate.ts +0 -113
  198. package/src/store/session/slices/chat/helpers.ts +0 -9
  199. package/src/store/session/slices/chat/initialState.ts +0 -13
  200. package/src/store/session/slices/chat/reducers/message.test.ts +0 -319
  201. package/src/store/session/slices/chat/reducers/message.ts +0 -156
  202. package/src/store/session/slices/chat/reducers/topic.test.ts +0 -179
  203. package/src/store/session/slices/chat/reducers/topic.ts +0 -81
  204. package/src/store/session/slices/chat/selectors/chat.ts +0 -111
  205. package/src/store/session/slices/chat/selectors/index.ts +0 -26
  206. package/src/store/session/slices/chat/selectors/topic.ts +0 -30
  207. package/src/store/session/slices/chat/selectors/utils.test.ts +0 -385
  208. package/src/store/session/slices/chat/selectors/utils.ts +0 -91
  209. package/src/store/session/slices/chat/utils.ts +0 -13
  210. package/src/store/session/slices/session/reducers/session.test.ts +0 -577
  211. package/src/store/session/slices/session/reducers/session.ts +0 -182
  212. package/src/types/database/db.ts +0 -4
  213. /package/src/store/{session/slices/chat → chat}/reducers/files.test.ts +0 -0
  214. /package/src/store/{session/slices/chat → chat}/reducers/files.ts +0 -0
@@ -0,0 +1,250 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { CreateMessageParams, MessageModel } from '@/database/models/message';
4
+
5
+ import { CreateTopicParams, QueryTopicParams, TopicModel } from '../topic';
6
+
7
+ describe('TopicModel', () => {
8
+ let topicData: CreateTopicParams;
9
+
10
+ beforeEach(() => {
11
+ // Set up topic data with the correct structure
12
+ topicData = {
13
+ sessionId: 'session1',
14
+ title: 'Test Topic',
15
+ favorite: false,
16
+ };
17
+ });
18
+
19
+ afterEach(async () => {
20
+ // Clean up the database after each test
21
+ await TopicModel.clearTable();
22
+ });
23
+ describe('create', () => {
24
+ it('should create a topic record', async () => {
25
+ const result = await TopicModel.create(topicData);
26
+
27
+ expect(result).toHaveProperty('id');
28
+ // Verify that the topic has been added to the database
29
+ const topicInDb = await TopicModel.findById(result.id);
30
+
31
+ expect(topicInDb).toEqual(
32
+ expect.objectContaining({
33
+ title: topicData.title,
34
+ favorite: topicData.favorite ? 1 : 0,
35
+ sessionId: topicData.sessionId,
36
+ }),
37
+ );
38
+ });
39
+
40
+ it('should create a topic with favorite set to true', async () => {
41
+ const favoriteTopicData: CreateTopicParams = {
42
+ ...topicData,
43
+ favorite: true,
44
+ };
45
+ const result = await TopicModel.create(favoriteTopicData);
46
+
47
+ expect(result).toHaveProperty('id');
48
+ const topicInDb = await TopicModel.findById(result.id);
49
+ expect(topicInDb).toEqual(
50
+ expect.objectContaining({
51
+ title: favoriteTopicData.title,
52
+ favorite: 1,
53
+ sessionId: favoriteTopicData.sessionId,
54
+ }),
55
+ );
56
+ });
57
+ });
58
+ describe('batch create', () => {
59
+ it('should batch create topic records', async () => {
60
+ const topicsToCreate = [topicData, topicData];
61
+ const results = await TopicModel.batchCreate(topicsToCreate);
62
+
63
+ expect(results.ids).toHaveLength(topicsToCreate.length);
64
+ // Verify that the topics have been added to the database
65
+ for (const result of results.ids!) {
66
+ const topicInDb = await TopicModel.findById(result);
67
+ expect(topicInDb).toEqual(
68
+ expect.objectContaining({
69
+ title: topicData.title,
70
+ favorite: topicData.favorite ? 1 : 0,
71
+ sessionId: topicData.sessionId,
72
+ }),
73
+ );
74
+ }
75
+ });
76
+
77
+ it('should batch create topics with mixed favorite values', async () => {
78
+ const mixedTopicsData: CreateTopicParams[] = [
79
+ { ...topicData, favorite: true },
80
+ { ...topicData, favorite: false },
81
+ ];
82
+
83
+ const results = await TopicModel.batchCreate(mixedTopicsData);
84
+
85
+ expect(results.ids).toHaveLength(mixedTopicsData.length);
86
+ for (const id of results.ids!) {
87
+ const topicInDb = await TopicModel.findById(id);
88
+ expect(topicInDb).toBeDefined();
89
+ expect(topicInDb.favorite).toBeGreaterThanOrEqual(0);
90
+ expect(topicInDb.favorite).toBeLessThanOrEqual(1);
91
+ }
92
+ });
93
+ });
94
+
95
+ it('should query topics with pagination', async () => {
96
+ // Create multiple topics to test the query method
97
+ await TopicModel.batchCreate([topicData, topicData]);
98
+
99
+ const queryParams: QueryTopicParams = { pageSize: 1, current: 0, sessionId: 'session1' };
100
+ const queriedTopics = await TopicModel.query(queryParams);
101
+
102
+ expect(queriedTopics).toHaveLength(1);
103
+ });
104
+
105
+ it('should find topics by session id', async () => {
106
+ // Create multiple topics to test the findBySessionId method
107
+ await TopicModel.batchCreate([topicData, topicData]);
108
+
109
+ const topicsBySessionId = await TopicModel.findBySessionId(topicData.sessionId);
110
+
111
+ expect(topicsBySessionId).toHaveLength(2);
112
+ expect(topicsBySessionId.every((i) => i.sessionId === topicData.sessionId)).toBeTruthy();
113
+ });
114
+
115
+ it('should delete a topic and its associated messages', async () => {
116
+ const createdTopic = await TopicModel.create(topicData);
117
+
118
+ await TopicModel.delete(createdTopic.id);
119
+
120
+ // Verify the topic and its related messages are deleted
121
+ const topicInDb = await TopicModel.findById(createdTopic.id);
122
+ expect(topicInDb).toBeUndefined();
123
+
124
+ // You need to verify that messages related to the topic are also deleted
125
+ // This would require additional setup to create messages associated with the topic
126
+ // and then assertions to check that they're deleted after the topic itself is deleted
127
+ });
128
+
129
+ it('should batch delete topics by session id', async () => {
130
+ // Create multiple topics to test the batchDeleteBySessionId method
131
+ await TopicModel.batchCreate([topicData, topicData]);
132
+
133
+ await TopicModel.batchDeleteBySessionId(topicData.sessionId);
134
+
135
+ // Verify that all topics with the given session id are deleted
136
+ const topicsInDb = await TopicModel.findBySessionId(topicData.sessionId);
137
+ expect(topicsInDb).toHaveLength(0);
138
+ });
139
+
140
+ it('should update a topic', async () => {
141
+ const createdTopic = await TopicModel.create(topicData);
142
+ const updateData = { title: 'New Title' };
143
+
144
+ await TopicModel.update(createdTopic.id, updateData);
145
+ const updatedTopic = await TopicModel.findById(createdTopic.id);
146
+
147
+ expect(updatedTopic).toHaveProperty('title', 'New Title');
148
+ });
149
+
150
+ describe('toggleFavorite', () => {
151
+ it('should toggle favorite status of a topic', async () => {
152
+ const createdTopic = await TopicModel.create(topicData);
153
+
154
+ const newState = await TopicModel.toggleFavorite(createdTopic.id);
155
+
156
+ expect(newState).toBe(true);
157
+ const topicInDb = await TopicModel.findById(createdTopic.id);
158
+ expect(topicInDb).toHaveProperty('favorite', 1);
159
+ });
160
+
161
+ it('should handle toggleFavorite when topic does not exist', async () => {
162
+ const nonExistentTopicId = 'non-existent-id';
163
+ await expect(TopicModel.toggleFavorite(nonExistentTopicId)).rejects.toThrow(
164
+ `Topic with id ${nonExistentTopicId} not found`,
165
+ );
166
+ });
167
+
168
+ it('should set favorite to specific state using toggleFavorite', async () => {
169
+ const createdTopic = await TopicModel.create(topicData);
170
+
171
+ // Set favorite to true regardless of current state
172
+ await TopicModel.toggleFavorite(createdTopic.id, true);
173
+ let topicInDb = await TopicModel.findById(createdTopic.id);
174
+ expect(topicInDb.favorite).toBe(1);
175
+
176
+ // Set favorite to false regardless of current state
177
+ await TopicModel.toggleFavorite(createdTopic.id, false);
178
+ topicInDb = await TopicModel.findById(createdTopic.id);
179
+ expect(topicInDb.favorite).toBe(0);
180
+ });
181
+ });
182
+
183
+ it('should delete a topic and its associated messages', async () => {
184
+ // 创建话题和相关联的消息
185
+ const createdTopic = await TopicModel.create(topicData);
186
+ const messageData: CreateMessageParams = {
187
+ content: 'Test Message',
188
+ topicId: createdTopic.id,
189
+ sessionId: topicData.sessionId,
190
+ role: 'user',
191
+ };
192
+ await MessageModel.create(messageData);
193
+
194
+ // 删除话题
195
+ await TopicModel.delete(createdTopic.id);
196
+
197
+ // 验证话题是否被删除
198
+ const topicInDb = await TopicModel.findById(createdTopic.id);
199
+ expect(topicInDb).toBeUndefined();
200
+
201
+ // 验证与话题关联的消息是否也被删除
202
+ const messagesInDb = await MessageModel.query({
203
+ sessionId: topicData.sessionId,
204
+ topicId: createdTopic.id,
205
+ });
206
+ expect(messagesInDb).toHaveLength(0);
207
+ });
208
+
209
+ it('should batch delete topics and their associated messages', async () => {
210
+ // 创建多个话题和相关联的消息
211
+ const createdTopic1 = await TopicModel.create(topicData);
212
+ const createdTopic2 = await TopicModel.create(topicData);
213
+
214
+ const messageData1: CreateMessageParams = {
215
+ content: 'Test Message 1',
216
+ topicId: createdTopic1.id,
217
+ sessionId: topicData.sessionId,
218
+ role: 'user',
219
+ };
220
+ const messageData2: CreateMessageParams = {
221
+ content: 'Test Message 2',
222
+ topicId: createdTopic2.id,
223
+ sessionId: topicData.sessionId,
224
+ role: 'user',
225
+ };
226
+ await MessageModel.create(messageData1);
227
+ await MessageModel.create(messageData2);
228
+
229
+ // 执行批量删除
230
+ await TopicModel.batchDelete([createdTopic1.id, createdTopic2.id]);
231
+
232
+ // 验证话题是否被删除
233
+ const topicInDb1 = await TopicModel.findById(createdTopic1.id);
234
+ const topicInDb2 = await TopicModel.findById(createdTopic2.id);
235
+ expect(topicInDb1).toBeUndefined();
236
+ expect(topicInDb2).toBeUndefined();
237
+
238
+ // 验证与话题关联的消息是否也被删除
239
+ const messagesInDb1 = await MessageModel.query({
240
+ sessionId: topicData.sessionId,
241
+ topicId: createdTopic1.id,
242
+ });
243
+ const messagesInDb2 = await MessageModel.query({
244
+ sessionId: topicData.sessionId,
245
+ topicId: createdTopic2.id,
246
+ });
247
+ expect(messagesInDb1).toHaveLength(0);
248
+ expect(messagesInDb2).toHaveLength(0);
249
+ });
250
+ });
@@ -1,17 +1,17 @@
1
- import { LocalFile, LocalFileSchema } from '@/types/database/files';
1
+ import { DB_File, DB_FileSchema } from '@/database/schemas/files';
2
2
  import { nanoid } from '@/utils/uuid';
3
3
 
4
4
  import { BaseModel } from '../core';
5
5
 
6
- class _FileModel extends BaseModel {
6
+ class _FileModel extends BaseModel<'files'> {
7
7
  constructor() {
8
- super('files', LocalFileSchema);
8
+ super('files', DB_FileSchema);
9
9
  }
10
10
 
11
- async create(file: LocalFile) {
11
+ async create(file: DB_File) {
12
12
  const id = nanoid();
13
13
 
14
- return this.add(file, `file-${id}`);
14
+ return this._add(file, `file-${id}`);
15
15
  }
16
16
 
17
17
  async findById(id: string) {
@@ -21,6 +21,10 @@ class _FileModel extends BaseModel {
21
21
  async delete(id: string) {
22
22
  return this.table.delete(id);
23
23
  }
24
+
25
+ async clear() {
26
+ return this.table.clear();
27
+ }
24
28
  }
25
29
 
26
30
  export const FileModel = new _FileModel();
@@ -0,0 +1,186 @@
1
+ import { DeepPartial } from 'utility-types';
2
+
3
+ import { BaseModel } from '@/database/core';
4
+ import { DBModel } from '@/database/core/types/db';
5
+ import { DB_Message, DB_MessageSchema } from '@/database/schemas/message';
6
+ import { ChatMessage } from '@/types/chatMessage';
7
+ import { nanoid } from '@/utils/uuid';
8
+
9
+ export interface CreateMessageParams
10
+ extends Partial<Omit<ChatMessage, 'content' | 'role'>>,
11
+ Pick<ChatMessage, 'content' | 'role'> {
12
+ sessionId: string;
13
+ }
14
+
15
+ export interface QueryMessageParams {
16
+ current?: number;
17
+ pageSize?: number;
18
+ sessionId: string;
19
+ topicId?: string;
20
+ }
21
+
22
+ class _MessageModel extends BaseModel {
23
+ constructor() {
24
+ super('messages', DB_MessageSchema);
25
+ }
26
+ async create(data: CreateMessageParams) {
27
+ const id = nanoid();
28
+
29
+ const messageData: DB_Message = this.mapChatMessageToDBMessage(data as ChatMessage);
30
+
31
+ return this._add(messageData, id);
32
+ }
33
+
34
+ async batchCreate(messages: ChatMessage[]) {
35
+ const data: DB_Message[] = messages.map((m) => this.mapChatMessageToDBMessage(m));
36
+
37
+ return this._batchAdd(data);
38
+ }
39
+
40
+ async query({
41
+ sessionId,
42
+ topicId,
43
+ pageSize = 9999,
44
+ current = 0,
45
+ }: QueryMessageParams): Promise<ChatMessage[]> {
46
+ const offset = current * pageSize;
47
+
48
+ const query =
49
+ topicId !== undefined
50
+ ? // TODO: The query {"sessionId":"xxx","topicId":"xxx"} on messages would benefit of a compound index [sessionId+topicId]
51
+ this.table.where({ sessionId, topicId }) // Use a compound index
52
+ : this.table
53
+ .where('sessionId')
54
+ .equals(sessionId)
55
+ .and((message) => !message.topicId);
56
+
57
+ const dbMessages: DBModel<DB_Message>[] = await query
58
+ .sortBy('createdAt')
59
+ // handle page size
60
+ .then((sortedArray) => sortedArray.slice(offset, offset + pageSize));
61
+
62
+ const messages = dbMessages.map((msg) => this.mapToChatMessage(msg));
63
+
64
+ const finalList: ChatMessage[] = [];
65
+
66
+ const addItem = (item: ChatMessage) => {
67
+ const isExist = finalList.findIndex((i) => item.id === i.id) > -1;
68
+ if (!isExist) {
69
+ finalList.push(item);
70
+ }
71
+ };
72
+ const messageMap = new Map<string, ChatMessage>();
73
+ for (const item of messages) messageMap.set(item.id, item);
74
+
75
+ for (const item of messages) {
76
+ if (!item.parentId || !messageMap.has(item.parentId)) {
77
+ // 如果消息没有父消息或者父消息不在列表中,直接添加
78
+ addItem(item);
79
+ } else {
80
+ // 如果消息有父消息,确保先添加父消息
81
+ addItem(messageMap.get(item.parentId)!);
82
+ addItem(item);
83
+ }
84
+ }
85
+ return finalList;
86
+ }
87
+
88
+ async findById(id: string): Promise<DBModel<DB_Message>> {
89
+ return this.table.get(id);
90
+ }
91
+
92
+ async delete(id: string) {
93
+ return this.table.delete(id);
94
+ }
95
+
96
+ async clearTable() {
97
+ return this.table.clear();
98
+ }
99
+
100
+ async update(id: string, data: DeepPartial<DB_Message>) {
101
+ return super._update(id, data);
102
+ }
103
+
104
+ /**
105
+ * Batch updates multiple fields of the specified messages.
106
+ *
107
+ * @param {string[]} messageIds - The identifiers of the messages to be updated.
108
+ * @param {Partial<DB_Message>} updateFields - An object containing the fields to update and their new values.
109
+ * @returns {Promise<number>} - The number of updated messages.
110
+ */
111
+ async batchUpdate(messageIds: string[], updateFields: Partial<DB_Message>): Promise<number> {
112
+ // Retrieve the messages by their IDs
113
+ const messagesToUpdate = await this.table.where(':id').anyOf(messageIds).toArray();
114
+
115
+ // Update the specified fields of each message
116
+ const updatedMessages = messagesToUpdate.map((message) => ({
117
+ ...message,
118
+ ...updateFields,
119
+ }));
120
+
121
+ // Use the bulkPut method to update the messages in bulk
122
+ await this.table.bulkPut(updatedMessages);
123
+
124
+ return updatedMessages.length;
125
+ }
126
+
127
+ /**
128
+ * Deletes multiple messages based on the assistantId and optionally the topicId.
129
+ * If topicId is not provided, it deletes messages where topicId is undefined.
130
+ * If topicId is provided, it deletes messages with that specific topicId.
131
+ *
132
+ * @param {string} sessionId - The identifier of the assistant associated with the messages.
133
+ * @param {string | undefined} topicId - The identifier of the topic associated with the messages (optional).
134
+ * @returns {Promise<void>}
135
+ */
136
+ async batchDelete(sessionId: string, topicId: string | undefined): Promise<void> {
137
+ // If topicId is specified, use both assistantId and topicId as the filter criteria in the query.
138
+ // Otherwise, filter by assistantId and require that topicId is undefined.
139
+ const query =
140
+ topicId !== undefined
141
+ ? this.table.where({ sessionId, topicId }) // Use a compound index
142
+ : this.table
143
+ .where('sessionId')
144
+ .equals(sessionId)
145
+ .and((message) => message.topicId === undefined);
146
+
147
+ // Retrieve a collection of message IDs that satisfy the criteria
148
+ const messageIds = await query.primaryKeys();
149
+
150
+ // Use the bulkDelete method to delete all selected messages in bulk
151
+ return this.table.bulkDelete(messageIds);
152
+ }
153
+
154
+ async queryAll() {
155
+ const data: DBModel<DB_Message>[] = await this.table.orderBy('updatedAt').toArray();
156
+
157
+ return data.map((element) => this.mapToChatMessage(element));
158
+ }
159
+
160
+ async isEmpty() {
161
+ const count = await this.table.count();
162
+
163
+ return count === 0;
164
+ }
165
+
166
+ private mapChatMessageToDBMessage(message: ChatMessage): DB_Message {
167
+ const { extra, ...messageData } = message;
168
+
169
+ return { ...messageData, ...extra } as DB_Message;
170
+ }
171
+
172
+ private mapToChatMessage = ({
173
+ fromModel,
174
+ translate,
175
+ tts,
176
+ ...item
177
+ }: DBModel<DB_Message>): ChatMessage => {
178
+ return {
179
+ ...item,
180
+ extra: { fromModel: fromModel, translate: translate, tts: tts },
181
+ meta: {},
182
+ };
183
+ };
184
+ }
185
+
186
+ export const MessageModel = new _MessageModel();
@@ -0,0 +1,160 @@
1
+ import { DeepPartial } from 'utility-types';
2
+
3
+ import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session';
4
+ import { BaseModel } from '@/database/core';
5
+ import { DB_Session, DB_SessionSchema } from '@/database/schemas/session';
6
+ import { LobeAgentConfig } from '@/types/agent';
7
+ import { LobeAgentSession, LobeSessions, SessionGroupKey } from '@/types/session';
8
+ import { merge } from '@/utils/merge';
9
+ import { uuid } from '@/utils/uuid';
10
+
11
+ class _SessionModel extends BaseModel {
12
+ constructor() {
13
+ super('sessions', DB_SessionSchema);
14
+ }
15
+
16
+ async create(type: 'agent' | 'group', defaultValue: Partial<LobeAgentSession>, id = uuid()) {
17
+ const data = merge(DEFAULT_AGENT_LOBE_SESSION, { type, ...defaultValue });
18
+ return this._add(data, id);
19
+ }
20
+
21
+ async batchCreate(sessions: LobeAgentSession[]) {
22
+ return this._batchAdd(sessions, { idGenerator: uuid });
23
+ }
24
+
25
+ async query({
26
+ pageSize = 9999,
27
+ current = 0,
28
+ }: { current?: number; pageSize?: number } = {}): Promise<LobeSessions> {
29
+ const offset = current * pageSize;
30
+
31
+ return this.table.orderBy('updatedAt').reverse().offset(offset).limit(pageSize).toArray();
32
+ }
33
+
34
+ /**
35
+ * get sessions by group
36
+ * @param group
37
+ */
38
+ async queryByGroup(group: SessionGroupKey) {
39
+ return this.table.where('group').equals(group).toArray();
40
+ }
41
+
42
+ async update(id: string, data: Partial<DB_Session>) {
43
+ return super._update(id, data);
44
+ }
45
+
46
+ async updateConfig(id: string, data: DeepPartial<LobeAgentConfig>) {
47
+ const session = await this.findById(id);
48
+ if (!session) return;
49
+
50
+ const config = merge(session.config, data);
51
+
52
+ return this.update(id, { config });
53
+ }
54
+ /**
55
+ * Delete a session , also delete all messages and topic associated with it.
56
+ */
57
+ async delete(id: string) {
58
+ return this.db.transaction('rw', [this.table, this.db.topics, this.db.messages], async () => {
59
+ // Delete all topics associated with the session
60
+ const topics = await this.db.topics.where('sessionId').equals(id).toArray();
61
+ const topicIds = topics.map((topic) => topic.id);
62
+ if (topicIds.length > 0) {
63
+ await this.db.topics.bulkDelete(topicIds);
64
+ }
65
+
66
+ // Delete all messages associated with the session
67
+ const messages = await this.db.messages.where('sessionId').equals(id).toArray();
68
+ const messageIds = messages.map((message) => message.id);
69
+ if (messageIds.length > 0) {
70
+ await this.db.messages.bulkDelete(messageIds);
71
+ }
72
+
73
+ // Finally, delete the session itself
74
+ await this.table.delete(id);
75
+ });
76
+ }
77
+
78
+ async clearTable() {
79
+ return this.table.clear();
80
+ }
81
+
82
+ async findById(id: string) {
83
+ return this.table.get(id);
84
+ }
85
+
86
+ async isEmpty() {
87
+ return (await this.table.count()) === 0;
88
+ }
89
+
90
+ /**
91
+ * Query sessions by keyword in title, description, content, or translated content
92
+ * @param keyword The keyword to search for
93
+ */
94
+ async queryByKeyword(keyword: string): Promise<LobeSessions> {
95
+ if (!keyword) return [];
96
+
97
+ console.time('queryByKeyword');
98
+ const keywordLowerCase = keyword.toLowerCase();
99
+
100
+ // First, filter sessions by title and description
101
+ const matchingSessionsPromise = this.table
102
+ .filter((session) => {
103
+ return (
104
+ session.meta.title.toLowerCase().includes(keywordLowerCase) ||
105
+ session.meta.description.toLowerCase().includes(keywordLowerCase)
106
+ );
107
+ })
108
+ .toArray();
109
+
110
+ // Next, find message IDs that contain the keyword in content or translated content
111
+ const matchingMessagesPromise = this.db.messages
112
+ .filter((message) => {
113
+ // check content
114
+ if (message.content.toLowerCase().includes(keywordLowerCase)) return true;
115
+
116
+ // check translate content
117
+ if (message.translate && message.translate.content) {
118
+ return message.translate.content.toLowerCase().includes(keywordLowerCase);
119
+ }
120
+
121
+ return false;
122
+ })
123
+ .toArray();
124
+
125
+ // match topics
126
+ const matchingTopicsPromise = this.db.topics
127
+ .filter((topic) => {
128
+ return topic.title.toLowerCase().includes(keywordLowerCase);
129
+ })
130
+ .toArray();
131
+
132
+ // Resolve both promises
133
+ const [matchingSessions, matchingMessages, matchingTopics] = await Promise.all([
134
+ matchingSessionsPromise,
135
+ matchingMessagesPromise,
136
+ matchingTopicsPromise,
137
+ ]);
138
+
139
+ const sessionIdsFromMessages = matchingMessages.map((message) => message.sessionId);
140
+ const sessionIdsFromTopics = matchingTopics.map((topic) => topic.sessionId);
141
+
142
+ // Combine session IDs from both sources
143
+ const combinedSessionIds = new Set([
144
+ ...sessionIdsFromMessages,
145
+ ...sessionIdsFromTopics,
146
+ ...matchingSessions.map((session) => session.id),
147
+ ]);
148
+
149
+ // Retrieve unique sessions by IDs
150
+ const data = await this.table
151
+ .where('id')
152
+ .anyOf([...combinedSessionIds])
153
+ .toArray();
154
+
155
+ console.timeEnd('queryByKeyword');
156
+ return data;
157
+ }
158
+ }
159
+
160
+ export const SessionModel = new _SessionModel();