@lobehub/chat 1.76.1 → 1.77.1

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 (131) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/common.json +13 -2
  4. package/locales/ar/error.json +10 -0
  5. package/locales/ar/models.json +9 -6
  6. package/locales/ar/setting.json +28 -0
  7. package/locales/bg-BG/common.json +13 -2
  8. package/locales/bg-BG/error.json +10 -0
  9. package/locales/bg-BG/models.json +9 -6
  10. package/locales/bg-BG/setting.json +28 -0
  11. package/locales/de-DE/common.json +13 -2
  12. package/locales/de-DE/error.json +10 -0
  13. package/locales/de-DE/models.json +9 -6
  14. package/locales/de-DE/setting.json +28 -0
  15. package/locales/en-US/common.json +13 -2
  16. package/locales/en-US/error.json +10 -0
  17. package/locales/en-US/models.json +9 -6
  18. package/locales/en-US/setting.json +28 -0
  19. package/locales/es-ES/common.json +13 -2
  20. package/locales/es-ES/error.json +10 -0
  21. package/locales/es-ES/models.json +9 -6
  22. package/locales/es-ES/setting.json +28 -0
  23. package/locales/fa-IR/common.json +13 -2
  24. package/locales/fa-IR/error.json +10 -0
  25. package/locales/fa-IR/models.json +9 -6
  26. package/locales/fa-IR/setting.json +28 -0
  27. package/locales/fr-FR/common.json +13 -2
  28. package/locales/fr-FR/error.json +10 -0
  29. package/locales/fr-FR/models.json +9 -6
  30. package/locales/fr-FR/setting.json +28 -0
  31. package/locales/it-IT/common.json +13 -2
  32. package/locales/it-IT/error.json +10 -0
  33. package/locales/it-IT/models.json +9 -6
  34. package/locales/it-IT/setting.json +28 -0
  35. package/locales/ja-JP/common.json +13 -2
  36. package/locales/ja-JP/error.json +10 -0
  37. package/locales/ja-JP/models.json +9 -6
  38. package/locales/ja-JP/setting.json +28 -0
  39. package/locales/ko-KR/common.json +13 -2
  40. package/locales/ko-KR/error.json +10 -0
  41. package/locales/ko-KR/models.json +9 -6
  42. package/locales/ko-KR/setting.json +28 -0
  43. package/locales/nl-NL/common.json +13 -2
  44. package/locales/nl-NL/error.json +10 -0
  45. package/locales/nl-NL/models.json +9 -6
  46. package/locales/nl-NL/setting.json +28 -0
  47. package/locales/pl-PL/common.json +13 -2
  48. package/locales/pl-PL/error.json +10 -0
  49. package/locales/pl-PL/models.json +9 -6
  50. package/locales/pl-PL/setting.json +28 -0
  51. package/locales/pt-BR/common.json +13 -2
  52. package/locales/pt-BR/error.json +10 -0
  53. package/locales/pt-BR/models.json +9 -6
  54. package/locales/pt-BR/setting.json +28 -0
  55. package/locales/ru-RU/common.json +13 -2
  56. package/locales/ru-RU/error.json +10 -0
  57. package/locales/ru-RU/models.json +9 -6
  58. package/locales/ru-RU/setting.json +28 -0
  59. package/locales/tr-TR/common.json +13 -2
  60. package/locales/tr-TR/error.json +10 -0
  61. package/locales/tr-TR/models.json +9 -6
  62. package/locales/tr-TR/setting.json +28 -0
  63. package/locales/vi-VN/common.json +13 -2
  64. package/locales/vi-VN/error.json +10 -0
  65. package/locales/vi-VN/models.json +9 -6
  66. package/locales/vi-VN/setting.json +28 -0
  67. package/locales/zh-CN/common.json +13 -2
  68. package/locales/zh-CN/error.json +10 -0
  69. package/locales/zh-CN/models.json +10 -7
  70. package/locales/zh-CN/setting.json +28 -0
  71. package/locales/zh-TW/common.json +13 -2
  72. package/locales/zh-TW/error.json +10 -0
  73. package/locales/zh-TW/models.json +9 -6
  74. package/locales/zh-TW/setting.json +28 -0
  75. package/package.json +1 -1
  76. package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +2 -2
  77. package/src/app/[variants]/(main)/chat/features/Migration/UpgradeButton.tsx +2 -1
  78. package/src/app/[variants]/(main)/settings/common/features/Common.tsx +0 -44
  79. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +40 -14
  80. package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +108 -0
  81. package/src/app/[variants]/(main)/settings/storage/IndexedDBStorage.tsx +55 -0
  82. package/src/app/[variants]/(main)/settings/storage/page.tsx +17 -0
  83. package/src/components/GroupIcon/index.tsx +25 -0
  84. package/src/components/IndexCard/index.tsx +143 -0
  85. package/src/components/ProgressItem/index.tsx +75 -0
  86. package/src/database/models/__tests__/session.test.ts +21 -0
  87. package/src/database/repositories/dataExporter/index.test.ts +330 -0
  88. package/src/database/repositories/dataExporter/index.ts +216 -0
  89. package/src/database/repositories/dataImporter/__tests__/fixtures/agents.json +65 -0
  90. package/src/database/repositories/dataImporter/__tests__/fixtures/agentsToSessions.json +541 -0
  91. package/src/database/repositories/dataImporter/__tests__/fixtures/topic.json +269 -0
  92. package/src/database/repositories/dataImporter/__tests__/fixtures/userSettings.json +18 -0
  93. package/src/database/repositories/dataImporter/__tests__/fixtures/with-client-id.json +778 -0
  94. package/src/database/repositories/dataImporter/__tests__/index.test.ts +120 -880
  95. package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +940 -0
  96. package/src/database/repositories/dataImporter/deprecated/index.ts +326 -0
  97. package/src/database/repositories/dataImporter/index.ts +684 -289
  98. package/src/database/server/models/session.ts +85 -9
  99. package/src/features/DataImporter/ImportDetail.tsx +203 -0
  100. package/src/features/DataImporter/SuccessResult.tsx +22 -6
  101. package/src/features/DataImporter/_deprecated.ts +43 -0
  102. package/src/features/DataImporter/config.ts +21 -0
  103. package/src/features/DataImporter/index.tsx +112 -31
  104. package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +6 -0
  105. package/src/features/User/UserPanel/useMenu.tsx +1 -36
  106. package/src/features/User/__tests__/useMenu.test.tsx +0 -2
  107. package/src/locales/default/common.ts +12 -1
  108. package/src/locales/default/error.ts +10 -0
  109. package/src/locales/default/setting.ts +28 -0
  110. package/src/server/routers/lambda/exporter.ts +25 -0
  111. package/src/server/routers/lambda/importer.ts +19 -3
  112. package/src/server/routers/lambda/index.ts +2 -0
  113. package/src/services/config.ts +80 -135
  114. package/src/services/export/_deprecated.ts +155 -0
  115. package/src/services/export/client.ts +15 -0
  116. package/src/services/export/index.ts +6 -0
  117. package/src/services/export/server.ts +9 -0
  118. package/src/services/export/type.ts +5 -0
  119. package/src/services/import/_deprecated.ts +42 -1
  120. package/src/services/import/client.test.ts +1 -1
  121. package/src/services/import/client.ts +30 -1
  122. package/src/services/import/server.ts +70 -2
  123. package/src/services/import/type.ts +10 -0
  124. package/src/store/global/initialState.ts +1 -0
  125. package/src/types/export.ts +11 -0
  126. package/src/types/exportConfig.ts +2 -0
  127. package/src/types/importer.ts +15 -0
  128. package/src/utils/client/exportFile.ts +21 -0
  129. package/vitest.config.ts +1 -1
  130. package/src/utils/config.ts +0 -109
  131. /package/src/database/repositories/dataImporter/{__tests__ → deprecated/__tests__}/fixtures/messages.json +0 -0
@@ -0,0 +1,326 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { and, eq, inArray } from 'drizzle-orm/expressions';
3
+
4
+ import {
5
+ agents,
6
+ agentsToSessions,
7
+ messagePlugins,
8
+ messageTranslates,
9
+ messages,
10
+ sessionGroups,
11
+ sessions,
12
+ topics,
13
+ } from '@/database/schemas';
14
+ import { LobeChatDatabase } from '@/database/type';
15
+ import { ImportResult } from '@/services/import/_deprecated';
16
+ import { ImporterEntryData } from '@/types/importer';
17
+ import { sanitizeUTF8 } from '@/utils/sanitizeUTF8';
18
+
19
+ export class DeprecatedDataImporterRepos {
20
+ private userId: string;
21
+ private db: LobeChatDatabase;
22
+
23
+ /**
24
+ * The version of the importer that this module supports
25
+ */
26
+ supportVersion = 7;
27
+
28
+ constructor(db: LobeChatDatabase, userId: string) {
29
+ this.userId = userId;
30
+ this.db = db;
31
+ }
32
+
33
+ importData = async (data: ImporterEntryData) => {
34
+ if (data.version > this.supportVersion) throw new Error('Unsupported version');
35
+
36
+ let sessionGroupResult: ImportResult = { added: 0, errors: 0, skips: 0 };
37
+ let sessionResult: ImportResult = { added: 0, errors: 0, skips: 0 };
38
+ let topicResult: ImportResult = { added: 0, errors: 0, skips: 0 };
39
+ let messageResult: ImportResult = { added: 0, errors: 0, skips: 0 };
40
+
41
+ let sessionGroupIdMap: Record<string, string> = {};
42
+ let sessionIdMap: Record<string, string> = {};
43
+ let topicIdMap: Record<string, string> = {};
44
+
45
+ await this.db.transaction(async (trx) => {
46
+ // import sessionGroups
47
+ if (data.sessionGroups && data.sessionGroups.length > 0) {
48
+ const query = await trx.query.sessionGroups.findMany({
49
+ where: and(
50
+ eq(sessionGroups.userId, this.userId),
51
+ inArray(
52
+ sessionGroups.clientId,
53
+ data.sessionGroups.map(({ id }) => id),
54
+ ),
55
+ ),
56
+ });
57
+
58
+ sessionGroupResult.skips = query.length;
59
+
60
+ const mapArray = await trx
61
+ .insert(sessionGroups)
62
+ .values(
63
+ data.sessionGroups.map(({ id, createdAt, updatedAt, ...res }) => ({
64
+ ...res,
65
+ clientId: id,
66
+ createdAt: new Date(createdAt),
67
+ updatedAt: new Date(updatedAt),
68
+ userId: this.userId,
69
+ })),
70
+ )
71
+ .onConflictDoUpdate({
72
+ set: { updatedAt: new Date() },
73
+ target: [sessionGroups.clientId, sessionGroups.userId],
74
+ })
75
+ .returning({ clientId: sessionGroups.clientId, id: sessionGroups.id });
76
+
77
+ sessionGroupResult.added = mapArray.length - query.length;
78
+
79
+ sessionGroupIdMap = Object.fromEntries(mapArray.map(({ clientId, id }) => [clientId, id]));
80
+ }
81
+
82
+ // import sessions
83
+ if (data.sessions && data.sessions.length > 0) {
84
+ const query = await trx.query.sessions.findMany({
85
+ where: and(
86
+ eq(sessions.userId, this.userId),
87
+ inArray(
88
+ sessions.clientId,
89
+ data.sessions.map(({ id }) => id),
90
+ ),
91
+ ),
92
+ });
93
+
94
+ sessionResult.skips = query.length;
95
+
96
+ const mapArray = await trx
97
+ .insert(sessions)
98
+ .values(
99
+ data.sessions.map(({ id, createdAt, updatedAt, group, ...res }) => ({
100
+ ...res,
101
+ clientId: id,
102
+ createdAt: new Date(createdAt),
103
+ groupId: group ? sessionGroupIdMap[group] : null,
104
+ updatedAt: new Date(updatedAt),
105
+ userId: this.userId,
106
+ })),
107
+ )
108
+ .onConflictDoUpdate({
109
+ set: { updatedAt: new Date() },
110
+ target: [sessions.clientId, sessions.userId],
111
+ })
112
+ .returning({ clientId: sessions.clientId, id: sessions.id });
113
+
114
+ // get the session client-server id map
115
+ sessionIdMap = Object.fromEntries(mapArray.map(({ clientId, id }) => [clientId, id]));
116
+
117
+ // update added count
118
+ sessionResult.added = mapArray.length - query.length;
119
+
120
+ const shouldInsertSessionAgents = data.sessions
121
+ // filter out existing session, only insert new ones
122
+ .filter((s) => query.every((q) => q.clientId !== s.id));
123
+
124
+ // 只有当需要有新的 session 时,才会插入 agent
125
+ if (shouldInsertSessionAgents.length > 0) {
126
+ const agentMapArray = await trx
127
+ .insert(agents)
128
+ .values(
129
+ shouldInsertSessionAgents.map(({ config, meta }) => ({
130
+ ...config,
131
+ ...meta,
132
+ userId: this.userId,
133
+ })),
134
+ )
135
+ .returning({ id: agents.id });
136
+
137
+ await trx.insert(agentsToSessions).values(
138
+ shouldInsertSessionAgents.map(({ id }, index) => ({
139
+ agentId: agentMapArray[index].id,
140
+ sessionId: sessionIdMap[id],
141
+ userId: this.userId,
142
+ })),
143
+ );
144
+ }
145
+ }
146
+
147
+ // import topics
148
+ if (data.topics && data.topics.length > 0) {
149
+ const skipQuery = await trx.query.topics.findMany({
150
+ where: and(
151
+ eq(topics.userId, this.userId),
152
+ inArray(
153
+ topics.clientId,
154
+ data.topics.map(({ id }) => id),
155
+ ),
156
+ ),
157
+ });
158
+ topicResult.skips = skipQuery.length;
159
+
160
+ const mapArray = await trx
161
+ .insert(topics)
162
+ .values(
163
+ data.topics.map(({ id, createdAt, updatedAt, sessionId, favorite, ...res }) => ({
164
+ ...res,
165
+ clientId: id,
166
+ createdAt: new Date(createdAt),
167
+ favorite: Boolean(favorite),
168
+ sessionId: sessionId ? sessionIdMap[sessionId] : null,
169
+ updatedAt: new Date(updatedAt),
170
+ userId: this.userId,
171
+ })),
172
+ )
173
+ .onConflictDoUpdate({
174
+ set: { updatedAt: new Date() },
175
+ target: [topics.clientId, topics.userId],
176
+ })
177
+ .returning({ clientId: topics.clientId, id: topics.id });
178
+
179
+ topicIdMap = Object.fromEntries(mapArray.map(({ clientId, id }) => [clientId, id]));
180
+
181
+ topicResult.added = mapArray.length - skipQuery.length;
182
+ }
183
+
184
+ // import messages
185
+ if (data.messages && data.messages.length > 0) {
186
+ // 1. find skip ones
187
+ console.time('find messages');
188
+ const skipQuery = await trx.query.messages.findMany({
189
+ where: and(
190
+ eq(messages.userId, this.userId),
191
+ inArray(
192
+ messages.clientId,
193
+ data.messages.map(({ id }) => id),
194
+ ),
195
+ ),
196
+ });
197
+ console.timeEnd('find messages');
198
+
199
+ messageResult.skips = skipQuery.length;
200
+
201
+ // filter out existing messages, only insert new ones
202
+ const shouldInsertMessages = data.messages.filter((s) =>
203
+ skipQuery.every((q) => q.clientId !== s.id),
204
+ );
205
+
206
+ // 2. insert messages
207
+ if (shouldInsertMessages.length > 0) {
208
+ const inertValues = shouldInsertMessages.map(
209
+ ({ id, extra, createdAt, updatedAt, sessionId, topicId, content, ...res }) => ({
210
+ ...res,
211
+ clientId: id,
212
+ content: sanitizeUTF8(content),
213
+ createdAt: new Date(createdAt),
214
+ model: extra?.fromModel,
215
+ parentId: null,
216
+ provider: extra?.fromProvider,
217
+ sessionId: sessionId ? sessionIdMap[sessionId] : null,
218
+ topicId: topicId ? topicIdMap[topicId] : null, // 暂时设为 NULL
219
+ updatedAt: new Date(updatedAt),
220
+ userId: this.userId,
221
+ }),
222
+ );
223
+
224
+ console.time('insert messages');
225
+ const BATCH_SIZE = 100; // 每批次插入的记录数
226
+
227
+ for (let i = 0; i < inertValues.length; i += BATCH_SIZE) {
228
+ const batch = inertValues.slice(i, i + BATCH_SIZE);
229
+ await trx.insert(messages).values(batch);
230
+ }
231
+
232
+ console.timeEnd('insert messages');
233
+
234
+ const messageIdArray = await trx
235
+ .select({ clientId: messages.clientId, id: messages.id })
236
+ .from(messages)
237
+ .where(
238
+ and(
239
+ eq(messages.userId, this.userId),
240
+ inArray(
241
+ messages.clientId,
242
+ data.messages.map(({ id }) => id),
243
+ ),
244
+ ),
245
+ );
246
+
247
+ const messageIdMap = Object.fromEntries(
248
+ messageIdArray.map(({ clientId, id }) => [clientId, id]),
249
+ );
250
+
251
+ // 3. update parentId for messages
252
+ console.time('execute updates parentId');
253
+ const parentIdUpdates = shouldInsertMessages
254
+ .filter((msg) => msg.parentId) // 只处理有 parentId 的消息
255
+ .map((msg) => {
256
+ if (messageIdMap[msg.parentId as string])
257
+ return sql`WHEN ${messages.clientId} = ${msg.id} THEN ${messageIdMap[msg.parentId as string]} `;
258
+
259
+ return undefined;
260
+ })
261
+ .filter(Boolean);
262
+
263
+ if (parentIdUpdates.length > 0) {
264
+ await trx
265
+ .update(messages)
266
+ .set({
267
+ parentId: sql`CASE ${sql.join(parentIdUpdates)} END`,
268
+ })
269
+ .where(
270
+ inArray(
271
+ messages.clientId,
272
+ data.messages.map((msg) => msg.id),
273
+ ),
274
+ );
275
+
276
+ // if needed, you can print the sql and params
277
+ // const SQL = updateQuery.toSQL();
278
+ // console.log('sql:', SQL.sql);
279
+ // console.log('params:', SQL.params);
280
+ }
281
+ console.timeEnd('execute updates parentId');
282
+
283
+ // 4. insert message plugins
284
+ const pluginInserts = shouldInsertMessages.filter((msg) => msg.plugin);
285
+ if (pluginInserts.length > 0) {
286
+ await trx.insert(messagePlugins).values(
287
+ pluginInserts.map((msg) => ({
288
+ apiName: msg.plugin?.apiName,
289
+ arguments: msg.plugin?.arguments,
290
+ id: messageIdMap[msg.id],
291
+ identifier: msg.plugin?.identifier,
292
+ state: msg.pluginState,
293
+ toolCallId: msg.tool_call_id,
294
+ type: msg.plugin?.type,
295
+ userId: this.userId,
296
+ })),
297
+ );
298
+ }
299
+
300
+ // 5. insert message translate
301
+ const translateInserts = shouldInsertMessages.filter((msg) => msg.extra?.translate);
302
+ if (translateInserts.length > 0) {
303
+ await trx.insert(messageTranslates).values(
304
+ translateInserts.map((msg) => ({
305
+ id: messageIdMap[msg.id],
306
+ ...msg.extra?.translate,
307
+ userId: this.userId,
308
+ })),
309
+ );
310
+ }
311
+
312
+ // TODO: 未来需要处理 TTS 和图片的插入 (目前存在 file 的部分,不方便处理)
313
+ }
314
+
315
+ messageResult.added = shouldInsertMessages.length;
316
+ }
317
+ });
318
+
319
+ return {
320
+ messages: messageResult,
321
+ sessionGroups: sessionGroupResult,
322
+ sessions: sessionResult,
323
+ topics: topicResult,
324
+ };
325
+ };
326
+ }