@lobehub/chat 0.162.25 → 0.164.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 (84) hide show
  1. package/.github/workflows/release.yml +21 -2
  2. package/.github/workflows/sync.yml +1 -1
  3. package/.github/workflows/test.yml +35 -4
  4. package/CHANGELOG.md +63 -0
  5. package/LICENSE +38 -21
  6. package/codecov.yml +11 -0
  7. package/drizzle.config.ts +29 -0
  8. package/next.config.mjs +3 -0
  9. package/package.json +24 -4
  10. package/scripts/migrateServerDB/index.ts +30 -0
  11. package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +2 -1
  12. package/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +95 -88
  13. package/src/app/(main)/chat/settings/features/HeaderContent.tsx +37 -31
  14. package/src/app/api/webhooks/clerk/__tests__/fixtures/createUser.json +73 -0
  15. package/src/app/api/webhooks/clerk/route.ts +159 -0
  16. package/src/app/api/webhooks/clerk/validateRequest.ts +22 -0
  17. package/src/app/trpc/edge/[trpc]/route.ts +1 -1
  18. package/src/app/trpc/lambda/[trpc]/route.ts +26 -0
  19. package/src/config/auth.ts +2 -0
  20. package/src/config/db.ts +13 -1
  21. package/src/database/server/core/db.ts +44 -0
  22. package/src/database/server/core/dbForTest.ts +45 -0
  23. package/src/database/server/index.ts +1 -0
  24. package/src/database/server/migrations/0000_init.sql +439 -0
  25. package/src/database/server/migrations/0001_add_client_id.sql +9 -0
  26. package/src/database/server/migrations/0002_amusing_puma.sql +9 -0
  27. package/src/database/server/migrations/meta/0000_snapshot.json +1583 -0
  28. package/src/database/server/migrations/meta/0001_snapshot.json +1636 -0
  29. package/src/database/server/migrations/meta/0002_snapshot.json +1630 -0
  30. package/src/database/server/migrations/meta/_journal.json +27 -0
  31. package/src/database/server/models/__tests__/file.test.ts +140 -0
  32. package/src/database/server/models/__tests__/message.test.ts +847 -0
  33. package/src/database/server/models/__tests__/plugin.test.ts +172 -0
  34. package/src/database/server/models/__tests__/session.test.ts +595 -0
  35. package/src/database/server/models/__tests__/topic.test.ts +623 -0
  36. package/src/database/server/models/__tests__/user.test.ts +173 -0
  37. package/src/database/server/models/_template.ts +44 -0
  38. package/src/database/server/models/file.ts +51 -0
  39. package/src/database/server/models/message.ts +378 -0
  40. package/src/database/server/models/plugin.ts +63 -0
  41. package/src/database/server/models/session.ts +290 -0
  42. package/src/database/server/models/sessionGroup.ts +69 -0
  43. package/src/database/server/models/topic.ts +265 -0
  44. package/src/database/server/models/user.ts +138 -0
  45. package/src/database/server/modules/DataImporter/__tests__/fixtures/messages.json +1101 -0
  46. package/src/database/server/modules/DataImporter/__tests__/index.test.ts +954 -0
  47. package/src/database/server/modules/DataImporter/index.ts +333 -0
  48. package/src/database/server/schemas/_id.ts +15 -0
  49. package/src/database/server/schemas/lobechat.ts +601 -0
  50. package/src/database/server/utils/idGenerator.test.ts +39 -0
  51. package/src/database/server/utils/idGenerator.ts +26 -0
  52. package/src/features/User/UserPanel/useMenu.tsx +43 -37
  53. package/src/libs/trpc/client.ts +52 -3
  54. package/src/server/files/s3.ts +21 -1
  55. package/src/server/keyVaultsEncrypt/index.test.ts +62 -0
  56. package/src/server/keyVaultsEncrypt/index.ts +93 -0
  57. package/src/server/mock.ts +1 -1
  58. package/src/server/routers/{index.ts → edge/index.ts} +3 -3
  59. package/src/server/routers/lambda/file.ts +49 -0
  60. package/src/server/routers/lambda/importer.ts +54 -0
  61. package/src/server/routers/lambda/index.ts +28 -0
  62. package/src/server/routers/lambda/message.ts +165 -0
  63. package/src/server/routers/lambda/plugin.ts +100 -0
  64. package/src/server/routers/lambda/session.ts +194 -0
  65. package/src/server/routers/lambda/sessionGroup.ts +77 -0
  66. package/src/server/routers/lambda/topic.ts +134 -0
  67. package/src/server/routers/lambda/user.ts +57 -0
  68. package/src/services/file/index.ts +4 -7
  69. package/src/services/file/server.ts +45 -0
  70. package/src/services/import/index.ts +4 -1
  71. package/src/services/import/server.ts +115 -0
  72. package/src/services/message/index.ts +4 -8
  73. package/src/services/message/server.ts +93 -0
  74. package/src/services/plugin/index.ts +4 -9
  75. package/src/services/plugin/server.ts +46 -0
  76. package/src/services/session/index.ts +4 -8
  77. package/src/services/session/server.ts +148 -0
  78. package/src/services/topic/index.ts +4 -9
  79. package/src/services/topic/server.ts +68 -0
  80. package/src/services/user/index.ts +4 -9
  81. package/src/services/user/server.ts +28 -0
  82. package/tests/setup-db.ts +7 -0
  83. package/vitest.config.ts +2 -1
  84. package/vitest.server.config.ts +23 -0
@@ -0,0 +1,333 @@
1
+ import { eq, inArray, sql } from 'drizzle-orm';
2
+ import { and } from 'drizzle-orm/expressions';
3
+
4
+ import { serverDB } from '@/database/server';
5
+ import {
6
+ agents,
7
+ agentsToSessions,
8
+ messagePlugins,
9
+ messageTranslates,
10
+ messages,
11
+ sessionGroups,
12
+ sessions,
13
+ topics,
14
+ } from '@/database/server/schemas/lobechat';
15
+ import { ImportResult } from '@/services/config';
16
+ import { ImporterEntryData } from '@/types/importer';
17
+
18
+ export class DataImporter {
19
+ private userId: string;
20
+
21
+ /**
22
+ * The version of the importer that this module supports
23
+ */
24
+ supportVersion = 7;
25
+
26
+ constructor(userId: string) {
27
+ this.userId = userId;
28
+ }
29
+
30
+ importData = async (data: ImporterEntryData) => {
31
+ if (data.version > this.supportVersion) throw new Error('Unsupported version');
32
+
33
+ let sessionGroupResult: ImportResult = { added: 0, errors: 0, skips: 0 };
34
+ let sessionResult: ImportResult = { added: 0, errors: 0, skips: 0 };
35
+ let topicResult: ImportResult = { added: 0, errors: 0, skips: 0 };
36
+ let messageResult: ImportResult = { added: 0, errors: 0, skips: 0 };
37
+
38
+ let sessionGroupIdMap: Record<string, string> = {};
39
+ let sessionIdMap: Record<string, string> = {};
40
+ let topicIdMap: Record<string, string> = {};
41
+
42
+ // import sessionGroups
43
+ await serverDB.transaction(async (trx) => {
44
+ if (data.sessionGroups && data.sessionGroups.length > 0) {
45
+ const query = await trx.query.sessionGroups.findMany({
46
+ where: and(
47
+ eq(sessionGroups.userId, this.userId),
48
+ inArray(
49
+ sessionGroups.clientId,
50
+ data.sessionGroups.map(({ id }) => id),
51
+ ),
52
+ ),
53
+ });
54
+
55
+ sessionGroupResult.skips = query.length;
56
+
57
+ const mapArray = await trx
58
+ .insert(sessionGroups)
59
+ .values(
60
+ data.sessionGroups.map(({ id, createdAt, updatedAt, ...res }) => ({
61
+ ...res,
62
+ clientId: id,
63
+ createdAt: new Date(createdAt),
64
+ updatedAt: new Date(updatedAt),
65
+ userId: this.userId,
66
+ })),
67
+ )
68
+ .onConflictDoUpdate({
69
+ set: { updatedAt: new Date() },
70
+ target: [sessionGroups.clientId, sessionGroups.userId],
71
+ })
72
+ .returning({ clientId: sessionGroups.clientId, id: sessionGroups.id })
73
+ .execute();
74
+
75
+ sessionGroupResult.added = mapArray.length - query.length;
76
+
77
+ sessionGroupIdMap = Object.fromEntries(mapArray.map(({ clientId, id }) => [clientId, id]));
78
+ }
79
+
80
+ // import sessions
81
+ if (data.sessions && data.sessions.length > 0) {
82
+ const query = await trx.query.sessions.findMany({
83
+ where: and(
84
+ eq(sessions.userId, this.userId),
85
+ inArray(
86
+ sessions.clientId,
87
+ data.sessions.map(({ id }) => id),
88
+ ),
89
+ ),
90
+ });
91
+
92
+ sessionResult.skips = query.length;
93
+
94
+ const mapArray = await trx
95
+ .insert(sessions)
96
+ .values(
97
+ data.sessions.map(({ id, createdAt, updatedAt, group, ...res }) => ({
98
+ ...res,
99
+ clientId: id,
100
+ createdAt: new Date(createdAt),
101
+ groupId: group ? sessionGroupIdMap[group] : null,
102
+ updatedAt: new Date(updatedAt),
103
+ userId: this.userId,
104
+ })),
105
+ )
106
+ .onConflictDoUpdate({
107
+ set: { updatedAt: new Date() },
108
+ target: [sessions.clientId, sessions.userId],
109
+ })
110
+ .returning({ clientId: sessions.clientId, id: sessions.id })
111
+ .execute();
112
+
113
+ // get the session client-server id map
114
+ sessionIdMap = Object.fromEntries(mapArray.map(({ clientId, id }) => [clientId, id]));
115
+
116
+ // update added count
117
+ sessionResult.added = mapArray.length - query.length;
118
+
119
+ const shouldInsertSessionAgents = data.sessions
120
+ // filter out existing session, only insert new ones
121
+ .filter((s) => query.every((q) => q.clientId !== s.id));
122
+
123
+ // 只有当需要有新的 session 时,才会插入 agent
124
+ if (shouldInsertSessionAgents.length > 0) {
125
+ const agentMapArray = await trx
126
+ .insert(agents)
127
+ .values(
128
+ shouldInsertSessionAgents.map(({ config, meta }) => ({
129
+ ...config,
130
+ ...meta,
131
+ userId: this.userId,
132
+ })),
133
+ )
134
+ .returning({ id: agents.id })
135
+ .execute();
136
+
137
+ await trx
138
+ .insert(agentsToSessions)
139
+ .values(
140
+ shouldInsertSessionAgents.map(({ id }, index) => ({
141
+ agentId: agentMapArray[index].id,
142
+ sessionId: sessionIdMap[id],
143
+ })),
144
+ )
145
+ .execute();
146
+ }
147
+ }
148
+
149
+ // import topics
150
+ if (data.topics && data.topics.length > 0) {
151
+ const skipQuery = await trx.query.topics.findMany({
152
+ where: and(
153
+ eq(topics.userId, this.userId),
154
+ inArray(
155
+ topics.clientId,
156
+ data.topics.map(({ id }) => id),
157
+ ),
158
+ ),
159
+ });
160
+ topicResult.skips = skipQuery.length;
161
+
162
+ const mapArray = await trx
163
+ .insert(topics)
164
+ .values(
165
+ data.topics.map(({ id, createdAt, updatedAt, sessionId, ...res }) => ({
166
+ ...res,
167
+ clientId: id,
168
+ createdAt: new Date(createdAt),
169
+ sessionId: sessionId ? sessionIdMap[sessionId] : null,
170
+ updatedAt: new Date(updatedAt),
171
+ userId: this.userId,
172
+ })),
173
+ )
174
+ .onConflictDoUpdate({
175
+ set: { updatedAt: new Date() },
176
+ target: [topics.clientId, topics.userId],
177
+ })
178
+ .returning({ clientId: topics.clientId, id: topics.id })
179
+ .execute();
180
+
181
+ topicIdMap = Object.fromEntries(mapArray.map(({ clientId, id }) => [clientId, id]));
182
+
183
+ topicResult.added = mapArray.length - skipQuery.length;
184
+ }
185
+
186
+ // import messages
187
+ if (data.messages && data.messages.length > 0) {
188
+ // 1. find skip ones
189
+ console.time('find messages');
190
+ const skipQuery = await trx.query.messages.findMany({
191
+ where: and(
192
+ eq(messages.userId, this.userId),
193
+ inArray(
194
+ messages.clientId,
195
+ data.messages.map(({ id }) => id),
196
+ ),
197
+ ),
198
+ });
199
+ console.timeEnd('find messages');
200
+
201
+ messageResult.skips = skipQuery.length;
202
+
203
+ // filter out existing messages, only insert new ones
204
+ const shouldInsertMessages = data.messages.filter((s) =>
205
+ skipQuery.every((q) => q.clientId !== s.id),
206
+ );
207
+
208
+ // 2. insert messages
209
+ if (shouldInsertMessages.length > 0) {
210
+ const inertValues = shouldInsertMessages.map(
211
+ ({ id, extra, createdAt, updatedAt, sessionId, topicId, ...res }) => ({
212
+ ...res,
213
+ clientId: id,
214
+ createdAt: new Date(createdAt),
215
+ model: extra?.fromModel,
216
+ parentId: null,
217
+ provider: extra?.fromProvider,
218
+ sessionId: sessionId ? sessionIdMap[sessionId] : null,
219
+ topicId: topicId ? topicIdMap[topicId] : null, // 暂时设为 NULL
220
+ updatedAt: new Date(updatedAt),
221
+ userId: this.userId,
222
+ }),
223
+ );
224
+
225
+ console.time('insert messages');
226
+ const BATCH_SIZE = 100; // 每批次插入的记录数
227
+
228
+ for (let i = 0; i < inertValues.length; i += BATCH_SIZE) {
229
+ const batch = inertValues.slice(i, i + BATCH_SIZE);
230
+ await trx.insert(messages).values(batch).execute();
231
+ }
232
+
233
+ console.timeEnd('insert messages');
234
+
235
+ const messageIdArray = await trx
236
+ .select({ clientId: messages.clientId, id: messages.id })
237
+ .from(messages)
238
+ .where(
239
+ and(
240
+ eq(messages.userId, this.userId),
241
+ inArray(
242
+ messages.clientId,
243
+ data.messages.map(({ id }) => id),
244
+ ),
245
+ ),
246
+ );
247
+
248
+ const messageIdMap = Object.fromEntries(
249
+ messageIdArray.map(({ clientId, id }) => [clientId, id]),
250
+ );
251
+
252
+ // 3. update parentId for messages
253
+ console.time('execute updates parentId');
254
+ const parentIdUpdates = shouldInsertMessages
255
+ .filter((msg) => msg.parentId) // 只处理有 parentId 的消息
256
+ .map((msg) => {
257
+ if (messageIdMap[msg.parentId as string])
258
+ return sql`WHEN ${messages.clientId} = ${msg.id} THEN ${messageIdMap[msg.parentId as string]} `;
259
+
260
+ return undefined;
261
+ })
262
+ .filter(Boolean);
263
+
264
+ if (parentIdUpdates.length > 0) {
265
+ const updateQuery = trx
266
+ .update(messages)
267
+ .set({
268
+ parentId: sql`CASE ${sql.join(parentIdUpdates)} END`,
269
+ })
270
+ .where(
271
+ inArray(
272
+ messages.clientId,
273
+ data.messages.map((msg) => msg.id),
274
+ ),
275
+ );
276
+
277
+ // if needed, you can print the sql and params
278
+ // const SQL = updateQuery.toSQL();
279
+ // console.log('sql:', SQL.sql);
280
+ // console.log('params:', SQL.params);
281
+
282
+ await updateQuery.execute();
283
+ }
284
+ console.timeEnd('execute updates parentId');
285
+
286
+ // 4. insert message plugins
287
+ const pluginInserts = shouldInsertMessages.filter((msg) => msg.plugin);
288
+ if (pluginInserts.length > 0) {
289
+ await trx
290
+ .insert(messagePlugins)
291
+ .values(
292
+ pluginInserts.map((msg) => ({
293
+ apiName: msg.plugin?.apiName,
294
+ arguments: msg.plugin?.arguments,
295
+ id: messageIdMap[msg.id],
296
+ identifier: msg.plugin?.identifier,
297
+ state: msg.pluginState,
298
+ toolCallId: msg.tool_call_id,
299
+ type: msg.plugin?.type,
300
+ })),
301
+ )
302
+ .execute();
303
+ }
304
+
305
+ // 5. insert message translate
306
+ const translateInserts = shouldInsertMessages.filter((msg) => msg.extra?.translate);
307
+ if (translateInserts.length > 0) {
308
+ await trx
309
+ .insert(messageTranslates)
310
+ .values(
311
+ translateInserts.map((msg) => ({
312
+ id: messageIdMap[msg.id],
313
+ ...msg.extra?.translate,
314
+ })),
315
+ )
316
+ .execute();
317
+ }
318
+
319
+ // TODO: 未来需要处理 TTS 和图片的插入 (目前存在 file 的部分,不方便处理)
320
+ }
321
+
322
+ messageResult.added = shouldInsertMessages.length;
323
+ }
324
+ });
325
+
326
+ return {
327
+ messages: messageResult,
328
+ sessionGroups: sessionGroupResult,
329
+ sessions: sessionResult,
330
+ topics: topicResult,
331
+ };
332
+ };
333
+ }
@@ -0,0 +1,15 @@
1
+ // refs: https://unkey.dev/blog/uuid-ux
2
+
3
+ // If I have 100 million users, each generating up to 1 million messages.
4
+ // Then the total number of IDs that need to be generated: 100 million × 1 million = 10^14 (100 trillion)
5
+ // 11-digit Nano ID: 36^11 ≈ 1.3 × 10^17 (130 trillion trillion)
6
+
7
+ export const FILE_ID_LENGTH = 19; // 5 prefix + 14 random, e.g. file_ydGX5gmaxL32fh
8
+
9
+ export const MESSAGE_ID_LENGTH = 18; // 4 prefix + 14 random, e.g. msg_GX5ymaxL3d2ds2
10
+
11
+ export const SESSION_ID_LENGTH = 16; // 4 prefix + 12 random, e.g. ssn_GX5y3d2dmaxL
12
+
13
+ export const TOPIC_ID_LENGTH = 16; // 4 prefix + 12 random, e.g. tpc_GX5ymd7axL3y
14
+
15
+ export const USER_ID_LENGTH = 14; // 4 prefix + 10 random, e.g. user_GXyxLmd75a