@lobehub/chat 1.77.5 → 1.77.7

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 (73) hide show
  1. package/.github/scripts/pr-comment.js +80 -0
  2. package/.github/scripts/pr-release-body.js +59 -0
  3. package/.github/workflows/release-desktop.yml +331 -0
  4. package/.github/workflows/test.yml +1 -1
  5. package/CHANGELOG.md +50 -0
  6. package/changelog/v1.json +18 -0
  7. package/next.config.ts +16 -11
  8. package/package.json +92 -89
  9. package/packages/electron-client-ipc/README.md +48 -0
  10. package/packages/electron-client-ipc/package.json +7 -0
  11. package/packages/electron-client-ipc/src/events/devtools.ts +6 -0
  12. package/packages/electron-client-ipc/src/events/index.ts +13 -0
  13. package/packages/electron-client-ipc/src/index.ts +2 -0
  14. package/packages/electron-client-ipc/src/types/dispatch.ts +10 -0
  15. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  16. package/packages/electron-server-ipc/README.md +1 -1
  17. package/pnpm-workspace.yaml +1 -0
  18. package/scripts/setup-test-postgres-db.sh +21 -0
  19. package/src/app/desktop/devtools/page.tsx +89 -0
  20. package/src/app/desktop/layout.tsx +31 -0
  21. package/src/app/layout.tsx +11 -0
  22. package/src/app/not-found.tsx +1 -0
  23. package/src/const/desktop.ts +1 -0
  24. package/src/const/version.ts +2 -0
  25. package/src/database/client/db.ts +3 -10
  26. package/src/database/core/db-adaptor.ts +20 -2
  27. package/src/database/models/__tests__/aiProvider.test.ts +2 -0
  28. package/src/database/models/__tests__/message.test.ts +97 -26
  29. package/src/database/models/__tests__/session.test.ts +2 -0
  30. package/src/database/models/drizzleMigration.ts +15 -0
  31. package/src/database/models/message.ts +10 -5
  32. package/src/database/models/session.ts +8 -0
  33. package/src/database/models/user.ts +10 -1
  34. package/src/database/server/index.ts +1 -1
  35. package/src/features/DevPanel/features/FloatPanel.tsx +23 -6
  36. package/src/features/User/UserPanel/index.tsx +10 -6
  37. package/src/libs/trpc/async/index.ts +11 -1
  38. package/src/libs/trpc/lambda/index.ts +1 -0
  39. package/src/libs/trpc/lambda/serverDatabase.ts +10 -0
  40. package/src/libs/trpc/middleware/userAuth.ts +10 -0
  41. package/src/server/routers/async/file.ts +4 -5
  42. package/src/server/routers/async/ragEval.ts +3 -4
  43. package/src/server/routers/lambda/_template.ts +3 -5
  44. package/src/server/routers/lambda/agent.ts +8 -8
  45. package/src/server/routers/lambda/aiModel.ts +5 -5
  46. package/src/server/routers/lambda/aiProvider.test.ts +0 -2
  47. package/src/server/routers/lambda/aiProvider.ts +5 -5
  48. package/src/server/routers/lambda/chunk.ts +18 -15
  49. package/src/server/routers/lambda/exporter.ts +4 -4
  50. package/src/server/routers/lambda/file.ts +5 -5
  51. package/src/server/routers/lambda/importer.ts +3 -3
  52. package/src/server/routers/lambda/knowledgeBase.ts +3 -3
  53. package/src/server/routers/lambda/message.ts +5 -3
  54. package/src/server/routers/lambda/plugin.ts +5 -3
  55. package/src/server/routers/lambda/ragEval.ts +17 -14
  56. package/src/server/routers/lambda/session.ts +6 -4
  57. package/src/server/routers/lambda/sessionGroup.ts +3 -3
  58. package/src/server/routers/lambda/thread.ts +4 -4
  59. package/src/server/routers/lambda/topic.ts +5 -3
  60. package/src/server/routers/lambda/user.ts +7 -7
  61. package/src/server/routers/tools/__tests__/search.test.ts +1 -0
  62. package/src/server/services/nextAuthUser/index.test.ts +1 -2
  63. package/src/server/translation.test.ts +72 -52
  64. package/src/server/translation.ts +2 -11
  65. package/src/services/electron/devtools.ts +9 -0
  66. package/src/styles/electron.ts +14 -0
  67. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/index.tsx +3 -8
  68. package/src/tools/web-browsing/Render/Search/SearchResult/ShowMore.tsx +2 -4
  69. package/src/types/electron.ts +11 -0
  70. package/src/utils/electron/dispatch.ts +10 -0
  71. package/tsconfig.json +6 -6
  72. package/vitest.config.ts +3 -1
  73. package/vitest.server.config.ts +7 -3
@@ -3,8 +3,8 @@ import { z } from 'zod';
3
3
  import { AiProviderModel } from '@/database/models/aiProvider';
4
4
  import { UserModel } from '@/database/models/user';
5
5
  import { AiInfraRepos } from '@/database/repositories/aiInfra';
6
- import { serverDB } from '@/database/server';
7
6
  import { authedProcedure, router } from '@/libs/trpc';
7
+ import { serverDatabase } from '@/libs/trpc/lambda';
8
8
  import { getServerGlobalConfig } from '@/server/globalConfig';
9
9
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
10
10
  import {
@@ -16,7 +16,7 @@ import {
16
16
  } from '@/types/aiProvider';
17
17
  import { ProviderConfig } from '@/types/user/settings';
18
18
 
19
- const aiProviderProcedure = authedProcedure.use(async (opts) => {
19
+ const aiProviderProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
20
20
  const { ctx } = opts;
21
21
 
22
22
  const { aiProvider } = await getServerGlobalConfig();
@@ -25,13 +25,13 @@ const aiProviderProcedure = authedProcedure.use(async (opts) => {
25
25
  return opts.next({
26
26
  ctx: {
27
27
  aiInfraRepos: new AiInfraRepos(
28
- serverDB,
28
+ ctx.serverDB,
29
29
  ctx.userId,
30
30
  aiProvider as Record<string, ProviderConfig>,
31
31
  ),
32
- aiProviderModel: new AiProviderModel(serverDB, ctx.userId),
32
+ aiProviderModel: new AiProviderModel(ctx.serverDB, ctx.userId),
33
33
  gateKeeper,
34
- userModel: new UserModel(serverDB, ctx.userId),
34
+ userModel: new UserModel(ctx.serverDB, ctx.userId),
35
35
  },
36
36
  });
37
37
  });
@@ -9,28 +9,31 @@ import { EmbeddingModel } from '@/database/models/embedding';
9
9
  import { FileModel } from '@/database/models/file';
10
10
  import { MessageModel } from '@/database/models/message';
11
11
  import { knowledgeBaseFiles } from '@/database/schemas';
12
- import { serverDB } from '@/database/server';
13
12
  import { authedProcedure, router } from '@/libs/trpc';
13
+ import { serverDatabase } from '@/libs/trpc/lambda';
14
14
  import { keyVaults } from '@/libs/trpc/middleware/keyVaults';
15
15
  import { getServerDefaultFilesConfig } from '@/server/globalConfig';
16
16
  import { initAgentRuntimeWithUserPayload } from '@/server/modules/AgentRuntime';
17
17
  import { ChunkService } from '@/server/services/chunk';
18
18
  import { SemanticSearchSchema } from '@/types/rag';
19
19
 
20
- const chunkProcedure = authedProcedure.use(keyVaults).use(async (opts) => {
21
- const { ctx } = opts;
22
-
23
- return opts.next({
24
- ctx: {
25
- asyncTaskModel: new AsyncTaskModel(serverDB, ctx.userId),
26
- chunkModel: new ChunkModel(serverDB, ctx.userId),
27
- chunkService: new ChunkService(ctx.userId),
28
- embeddingModel: new EmbeddingModel(serverDB, ctx.userId),
29
- fileModel: new FileModel(serverDB, ctx.userId),
30
- messageModel: new MessageModel(serverDB, ctx.userId),
31
- },
20
+ const chunkProcedure = authedProcedure
21
+ .use(serverDatabase)
22
+ .use(keyVaults)
23
+ .use(async (opts) => {
24
+ const { ctx } = opts;
25
+
26
+ return opts.next({
27
+ ctx: {
28
+ asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId),
29
+ chunkModel: new ChunkModel(ctx.serverDB, ctx.userId),
30
+ chunkService: new ChunkService(ctx.userId),
31
+ embeddingModel: new EmbeddingModel(ctx.serverDB, ctx.userId),
32
+ fileModel: new FileModel(ctx.serverDB, ctx.userId),
33
+ messageModel: new MessageModel(ctx.serverDB, ctx.userId),
34
+ },
35
+ });
32
36
  });
33
- });
34
37
 
35
38
  export const chunkRouter = router({
36
39
  createEmbeddingChunksTask: chunkProcedure
@@ -173,7 +176,7 @@ export const chunkRouter = router({
173
176
  let finalFileIds = input.fileIds ?? [];
174
177
 
175
178
  if (input.knowledgeIds && input.knowledgeIds.length > 0) {
176
- const knowledgeFiles = await serverDB.query.knowledgeBaseFiles.findMany({
179
+ const knowledgeFiles = await ctx.serverDB.query.knowledgeBaseFiles.findMany({
177
180
  where: inArray(knowledgeBaseFiles.knowledgeBaseId, input.knowledgeIds),
178
181
  });
179
182
 
@@ -1,13 +1,13 @@
1
1
  import { DrizzleMigrationModel } from '@/database/models/drizzleMigration';
2
2
  import { DataExporterRepos } from '@/database/repositories/dataExporter';
3
- import { serverDB } from '@/database/server';
4
3
  import { authedProcedure, router } from '@/libs/trpc';
4
+ import { serverDatabase } from '@/libs/trpc/lambda';
5
5
  import { ExportDatabaseData } from '@/types/export';
6
6
 
7
- const exportProcedure = authedProcedure.use(async (opts) => {
7
+ const exportProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
8
8
  const { ctx } = opts;
9
- const dataExporterRepos = new DataExporterRepos(serverDB, ctx.userId);
10
- const drizzleMigration = new DrizzleMigrationModel(serverDB);
9
+ const dataExporterRepos = new DataExporterRepos(ctx.serverDB, ctx.userId);
10
+ const drizzleMigration = new DrizzleMigrationModel(ctx.serverDB);
11
11
 
12
12
  return opts.next({
13
13
  ctx: { dataExporterRepos, drizzleMigration },
@@ -5,21 +5,21 @@ import { serverDBEnv } from '@/config/db';
5
5
  import { AsyncTaskModel } from '@/database/models/asyncTask';
6
6
  import { ChunkModel } from '@/database/models/chunk';
7
7
  import { FileModel } from '@/database/models/file';
8
- import { serverDB } from '@/database/server';
9
8
  import { authedProcedure, router } from '@/libs/trpc';
9
+ import { serverDatabase } from '@/libs/trpc/lambda';
10
10
  import { S3 } from '@/server/modules/S3';
11
11
  import { getFullFileUrl } from '@/server/utils/files';
12
12
  import { AsyncTaskStatus, AsyncTaskType } from '@/types/asyncTask';
13
13
  import { FileListItem, QueryFileListSchema, UploadFileSchema } from '@/types/files';
14
14
 
15
- const fileProcedure = authedProcedure.use(async (opts) => {
15
+ const fileProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
16
16
  const { ctx } = opts;
17
17
 
18
18
  return opts.next({
19
19
  ctx: {
20
- asyncTaskModel: new AsyncTaskModel(serverDB, ctx.userId),
21
- chunkModel: new ChunkModel(serverDB, ctx.userId),
22
- fileModel: new FileModel(serverDB, ctx.userId),
20
+ asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId),
21
+ chunkModel: new ChunkModel(ctx.serverDB, ctx.userId),
22
+ fileModel: new FileModel(ctx.serverDB, ctx.userId),
23
23
  },
24
24
  });
25
25
  });
@@ -2,15 +2,15 @@ import { TRPCError } from '@trpc/server';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import { DataImporterRepos } from '@/database/repositories/dataImporter';
5
- import { serverDB } from '@/database/server';
6
5
  import { authedProcedure, router } from '@/libs/trpc';
6
+ import { serverDatabase } from '@/libs/trpc/lambda';
7
7
  import { S3 } from '@/server/modules/S3';
8
8
  import { ImportPgDataStructure } from '@/types/export';
9
9
  import { ImportResultData, ImporterEntryData } from '@/types/importer';
10
10
 
11
- const importProcedure = authedProcedure.use(async (opts) => {
11
+ const importProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
12
12
  const { ctx } = opts;
13
- const dataImporterService = new DataImporterRepos(serverDB, ctx.userId);
13
+ const dataImporterService = new DataImporterRepos(ctx.serverDB, ctx.userId);
14
14
 
15
15
  return opts.next({
16
16
  ctx: { dataImporterService },
@@ -2,16 +2,16 @@ import { z } from 'zod';
2
2
 
3
3
  import { KnowledgeBaseModel } from '@/database/models/knowledgeBase';
4
4
  import { insertKnowledgeBasesSchema } from '@/database/schemas';
5
- import { serverDB } from '@/database/server';
6
5
  import { authedProcedure, router } from '@/libs/trpc';
6
+ import { serverDatabase } from '@/libs/trpc/lambda';
7
7
  import { KnowledgeBaseItem } from '@/types/knowledgeBase';
8
8
 
9
- const knowledgeBaseProcedure = authedProcedure.use(async (opts) => {
9
+ const knowledgeBaseProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
10
10
  const { ctx } = opts;
11
11
 
12
12
  return opts.next({
13
13
  ctx: {
14
- knowledgeBaseModel: new KnowledgeBaseModel(serverDB, ctx.userId),
14
+ knowledgeBaseModel: new KnowledgeBaseModel(ctx.serverDB, ctx.userId),
15
15
  },
16
16
  });
17
17
  });
@@ -2,19 +2,20 @@ import { z } from 'zod';
2
2
 
3
3
  import { MessageModel } from '@/database/models/message';
4
4
  import { updateMessagePluginSchema } from '@/database/schemas';
5
- import { serverDB } from '@/database/server';
5
+ import { getServerDB } from '@/database/server';
6
6
  import { authedProcedure, publicProcedure, router } from '@/libs/trpc';
7
+ import { serverDatabase } from '@/libs/trpc/lambda';
7
8
  import { getFullFileUrl } from '@/server/utils/files';
8
9
  import { ChatMessage } from '@/types/message';
9
10
  import { BatchTaskResult } from '@/types/service';
10
11
 
11
12
  type ChatMessageList = ChatMessage[];
12
13
 
13
- const messageProcedure = authedProcedure.use(async (opts) => {
14
+ const messageProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
14
15
  const { ctx } = opts;
15
16
 
16
17
  return opts.next({
17
- ctx: { messageModel: new MessageModel(serverDB, ctx.userId) },
18
+ ctx: { messageModel: new MessageModel(ctx.serverDB, ctx.userId) },
18
19
  });
19
20
  });
20
21
 
@@ -95,6 +96,7 @@ export const messageRouter = router({
95
96
  )
96
97
  .query(async ({ input, ctx }) => {
97
98
  if (!ctx.userId) return [];
99
+ const serverDB = await getServerDB();
98
100
 
99
101
  const messageModel = new MessageModel(serverDB, ctx.userId);
100
102
 
@@ -1,15 +1,16 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  import { PluginModel } from '@/database/models/plugin';
4
- import { serverDB } from '@/database/server';
4
+ import { getServerDB } from '@/database/server';
5
5
  import { authedProcedure, publicProcedure, router } from '@/libs/trpc';
6
+ import { serverDatabase } from '@/libs/trpc/lambda';
6
7
  import { LobeTool } from '@/types/tool';
7
8
 
8
- const pluginProcedure = authedProcedure.use(async (opts) => {
9
+ const pluginProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
9
10
  const { ctx } = opts;
10
11
 
11
12
  return opts.next({
12
- ctx: { pluginModel: new PluginModel(serverDB, ctx.userId) },
13
+ ctx: { pluginModel: new PluginModel(ctx.serverDB, ctx.userId) },
13
14
  });
14
15
  });
15
16
 
@@ -66,6 +67,7 @@ export const pluginRouter = router({
66
67
  getPlugins: publicProcedure.query(async ({ ctx }): Promise<LobeTool[]> => {
67
68
  if (!ctx.userId) return [];
68
69
 
70
+ const serverDB = await getServerDB();
69
71
  const pluginModel = new PluginModel(serverDB, ctx.userId);
70
72
 
71
73
  return pluginModel.query();
@@ -7,7 +7,6 @@ import { z } from 'zod';
7
7
 
8
8
  import { DEFAULT_EMBEDDING_MODEL, DEFAULT_MODEL } from '@/const/settings';
9
9
  import { FileModel } from '@/database/models/file';
10
- import { serverDB } from '@/database/server';
11
10
  import {
12
11
  EvalDatasetModel,
13
12
  EvalDatasetRecordModel,
@@ -15,6 +14,7 @@ import {
15
14
  EvaluationRecordModel,
16
15
  } from '@/database/server/models/ragEval';
17
16
  import { authedProcedure, router } from '@/libs/trpc';
17
+ import { serverDatabase } from '@/libs/trpc/lambda';
18
18
  import { keyVaults } from '@/libs/trpc/middleware/keyVaults';
19
19
  import { S3 } from '@/server/modules/S3';
20
20
  import { createAsyncServerClient } from '@/server/routers/async';
@@ -29,20 +29,23 @@ import {
29
29
  insertEvalEvaluationSchema,
30
30
  } from '@/types/eval';
31
31
 
32
- const ragEvalProcedure = authedProcedure.use(keyVaults).use(async (opts) => {
33
- const { ctx } = opts;
34
-
35
- return opts.next({
36
- ctx: {
37
- datasetModel: new EvalDatasetModel(ctx.userId),
38
- fileModel: new FileModel(serverDB, ctx.userId),
39
- datasetRecordModel: new EvalDatasetRecordModel(ctx.userId),
40
- evaluationModel: new EvalEvaluationModel(ctx.userId),
41
- evaluationRecordModel: new EvaluationRecordModel(ctx.userId),
42
- s3: new S3(),
43
- },
32
+ const ragEvalProcedure = authedProcedure
33
+ .use(serverDatabase)
34
+ .use(keyVaults)
35
+ .use(async (opts) => {
36
+ const { ctx } = opts;
37
+
38
+ return opts.next({
39
+ ctx: {
40
+ datasetModel: new EvalDatasetModel(ctx.userId),
41
+ fileModel: new FileModel(ctx.serverDB, ctx.userId),
42
+ datasetRecordModel: new EvalDatasetRecordModel(ctx.userId),
43
+ evaluationModel: new EvalEvaluationModel(ctx.userId),
44
+ evaluationRecordModel: new EvaluationRecordModel(ctx.userId),
45
+ s3: new S3(),
46
+ },
47
+ });
44
48
  });
45
- });
46
49
 
47
50
  export const ragEvalRouter = router({
48
51
  createDataset: ragEvalProcedure
@@ -3,21 +3,22 @@ import { z } from 'zod';
3
3
  import { SessionModel } from '@/database/models/session';
4
4
  import { SessionGroupModel } from '@/database/models/sessionGroup';
5
5
  import { insertAgentSchema, insertSessionSchema } from '@/database/schemas';
6
- import { serverDB } from '@/database/server';
6
+ import { getServerDB } from '@/database/server';
7
7
  import { authedProcedure, publicProcedure, router } from '@/libs/trpc';
8
+ import { serverDatabase } from '@/libs/trpc/lambda';
8
9
  import { AgentChatConfigSchema } from '@/types/agent';
9
10
  import { LobeMetaDataSchema } from '@/types/meta';
10
11
  import { BatchTaskResult } from '@/types/service';
11
12
  import { ChatSessionList } from '@/types/session';
12
13
  import { merge } from '@/utils/merge';
13
14
 
14
- const sessionProcedure = authedProcedure.use(async (opts) => {
15
+ const sessionProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
15
16
  const { ctx } = opts;
16
17
 
17
18
  return opts.next({
18
19
  ctx: {
19
- sessionGroupModel: new SessionGroupModel(serverDB, ctx.userId),
20
- sessionModel: new SessionModel(serverDB, ctx.userId),
20
+ sessionGroupModel: new SessionGroupModel(ctx.serverDB, ctx.userId),
21
+ sessionModel: new SessionModel(ctx.serverDB, ctx.userId),
21
22
  },
22
23
  });
23
24
  });
@@ -95,6 +96,7 @@ export const sessionRouter = router({
95
96
  sessions: [],
96
97
  };
97
98
 
99
+ const serverDB = await getServerDB();
98
100
  const sessionModel = new SessionModel(serverDB, ctx.userId);
99
101
 
100
102
  return sessionModel.queryWithGroups();
@@ -2,16 +2,16 @@ import { z } from 'zod';
2
2
 
3
3
  import { SessionGroupModel } from '@/database/models/sessionGroup';
4
4
  import { insertSessionGroupSchema } from '@/database/schemas';
5
- import { serverDB } from '@/database/server';
6
5
  import { authedProcedure, router } from '@/libs/trpc';
6
+ import { serverDatabase } from '@/libs/trpc/lambda';
7
7
  import { SessionGroupItem } from '@/types/session';
8
8
 
9
- const sessionProcedure = authedProcedure.use(async (opts) => {
9
+ const sessionProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
10
10
  const { ctx } = opts;
11
11
 
12
12
  return opts.next({
13
13
  ctx: {
14
- sessionGroupModel: new SessionGroupModel(serverDB, ctx.userId),
14
+ sessionGroupModel: new SessionGroupModel(ctx.serverDB, ctx.userId),
15
15
  },
16
16
  });
17
17
  });
@@ -3,17 +3,17 @@ import { z } from 'zod';
3
3
  import { MessageModel } from '@/database/models/message';
4
4
  import { ThreadModel } from '@/database/models/thread';
5
5
  import { insertThreadSchema } from '@/database/schemas';
6
- import { serverDB } from '@/database/server';
7
6
  import { authedProcedure, router } from '@/libs/trpc';
7
+ import { serverDatabase } from '@/libs/trpc/lambda';
8
8
  import { ThreadItem, createThreadSchema } from '@/types/topic/thread';
9
9
 
10
- const threadProcedure = authedProcedure.use(async (opts) => {
10
+ const threadProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
11
11
  const { ctx } = opts;
12
12
 
13
13
  return opts.next({
14
14
  ctx: {
15
- messageModel: new MessageModel(serverDB, ctx.userId),
16
- threadModel: new ThreadModel(serverDB, ctx.userId),
15
+ messageModel: new MessageModel(ctx.serverDB, ctx.userId),
16
+ threadModel: new ThreadModel(ctx.serverDB, ctx.userId),
17
17
  },
18
18
  });
19
19
  });
@@ -1,15 +1,16 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  import { TopicModel } from '@/database/models/topic';
4
- import { serverDB } from '@/database/server';
4
+ import { getServerDB } from '@/database/server';
5
5
  import { authedProcedure, publicProcedure, router } from '@/libs/trpc';
6
+ import { serverDatabase } from '@/libs/trpc/lambda';
6
7
  import { BatchTaskResult } from '@/types/service';
7
8
 
8
- const topicProcedure = authedProcedure.use(async (opts) => {
9
+ const topicProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
9
10
  const { ctx } = opts;
10
11
 
11
12
  return opts.next({
12
- ctx: { topicModel: new TopicModel(serverDB, ctx.userId) },
13
+ ctx: { topicModel: new TopicModel(ctx.serverDB, ctx.userId) },
13
14
  });
14
15
  });
15
16
 
@@ -101,6 +102,7 @@ export const topicRouter = router({
101
102
  .query(async ({ input, ctx }) => {
102
103
  if (!ctx.userId) return [];
103
104
 
105
+ const serverDB = await getServerDB();
104
106
  const topicModel = new TopicModel(serverDB, ctx.userId);
105
107
 
106
108
  return topicModel.query(input);
@@ -5,10 +5,10 @@ import { enableClerk } from '@/const/auth';
5
5
  import { MessageModel } from '@/database/models/message';
6
6
  import { SessionModel } from '@/database/models/session';
7
7
  import { UserModel, UserNotFoundError } from '@/database/models/user';
8
- import { serverDB } from '@/database/server';
9
8
  import { ClerkAuth } from '@/libs/clerk-auth';
10
9
  import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
11
10
  import { authedProcedure, router } from '@/libs/trpc';
11
+ import { serverDatabase } from '@/libs/trpc/lambda';
12
12
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
13
13
  import { UserService } from '@/server/services/user';
14
14
  import {
@@ -19,12 +19,12 @@ import {
19
19
  } from '@/types/user';
20
20
  import { UserSettings } from '@/types/user/settings';
21
21
 
22
- const userProcedure = authedProcedure.use(async (opts) => {
23
- return opts.next({
22
+ const userProcedure = authedProcedure.use(serverDatabase).use(async ({ ctx, next }) => {
23
+ return next({
24
24
  ctx: {
25
25
  clerkAuth: new ClerkAuth(),
26
- nextAuthDbAdapter: LobeNextAuthDbAdapter(serverDB),
27
- userModel: new UserModel(serverDB, opts.ctx.userId),
26
+ nextAuthDbAdapter: LobeNextAuthDbAdapter(ctx.serverDB),
27
+ userModel: new UserModel(ctx.serverDB, ctx.userId),
28
28
  },
29
29
  });
30
30
  });
@@ -77,10 +77,10 @@ export const userRouter = router({
77
77
  }
78
78
  }
79
79
 
80
- const messageModel = new MessageModel(serverDB, ctx.userId);
80
+ const messageModel = new MessageModel(ctx.serverDB, ctx.userId);
81
81
  const hasMoreThan4Messages = await messageModel.hasMoreThanN(4);
82
82
 
83
- const sessionModel = new SessionModel(serverDB, ctx.userId);
83
+ const sessionModel = new SessionModel(ctx.serverDB, ctx.userId);
84
84
  const hasAnyMessages = await messageModel.hasMoreThanN(0);
85
85
  const hasExtraSession = await sessionModel.hasMoreThanN(1);
86
86
 
@@ -21,6 +21,7 @@ vi.mock('@/config/tools', () => ({
21
21
 
22
22
  vi.mock('@/const/version', () => ({
23
23
  isServerMode: true,
24
+ isDesktop: false,
24
25
  }));
25
26
 
26
27
  const createCaller = createCallerFactory(searchRouter);
@@ -6,7 +6,6 @@ import { UserModel } from '@/database/models/user';
6
6
  import { UserItem } from '@/database/schemas';
7
7
  import { serverDB } from '@/database/server';
8
8
  import { pino } from '@/libs/logger';
9
- import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
10
9
 
11
10
  import { NextAuthUserService } from './index';
12
11
 
@@ -23,7 +22,7 @@ vi.mock('@/database/server');
23
22
  describe('NextAuthUserService', () => {
24
23
  let service: NextAuthUserService;
25
24
 
26
- beforeEach(() => {
25
+ beforeEach(async () => {
27
26
  vi.clearAllMocks();
28
27
  service = new NextAuthUserService();
29
28
  });
@@ -1,12 +1,9 @@
1
1
  // @vitest-environment node
2
2
  import { cookies } from 'next/headers';
3
- import * as fs from 'node:fs';
4
- import * as path from 'node:path';
5
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
6
4
 
7
- import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale';
5
+ import { DEFAULT_LANG } from '@/const/locale';
8
6
  import { normalizeLocale } from '@/locales/resources';
9
- import * as env from '@/utils/env';
10
7
 
11
8
  import { getLocale, translation } from './translation';
12
9
 
@@ -15,15 +12,6 @@ vi.mock('next/headers', () => ({
15
12
  cookies: vi.fn(),
16
13
  }));
17
14
 
18
- vi.mock('node:fs', () => ({
19
- existsSync: vi.fn(),
20
- readFileSync: vi.fn(),
21
- }));
22
-
23
- vi.mock('node:path', () => ({
24
- join: vi.fn(),
25
- }));
26
-
27
15
  vi.mock('@/const/locale', () => ({
28
16
  DEFAULT_LANG: 'en-US',
29
17
  LOBE_LOCALE_COOKIE: 'LOBE_LOCALE',
@@ -37,6 +25,28 @@ vi.mock('@/utils/env', () => ({
37
25
  isDev: false,
38
26
  }));
39
27
 
28
+ // 模拟动态导入结果
29
+ const mockTranslations = {
30
+ key1: 'Value 1',
31
+ key2: 'Value 2 with {{param}}',
32
+ nested: { key: 'Nested value' },
33
+ };
34
+
35
+ const mockDefaultTranslations = {
36
+ key1: '默认值 1',
37
+ key2: '默认值 2 带 {{param}}',
38
+ nested: { key: '默认嵌套值' },
39
+ };
40
+
41
+ // 重写导入函数
42
+ vi.mock('@/../locales/en-US/common.json', async () => {
43
+ return mockTranslations;
44
+ });
45
+
46
+ vi.mock('@/locales/default/common', async () => {
47
+ return mockDefaultTranslations;
48
+ });
49
+
40
50
  describe('getLocale', () => {
41
51
  const mockCookieStore = {
42
52
  get: vi.fn(),
@@ -61,17 +71,12 @@ describe('getLocale', () => {
61
71
  });
62
72
 
63
73
  describe('translation', () => {
64
- const mockTranslations = {
65
- key1: 'Value 1',
66
- key2: 'Value 2 with {{param}}',
67
- nested: { key: 'Nested value' },
68
- };
69
-
70
74
  beforeEach(() => {
71
75
  vi.clearAllMocks();
72
- (fs.existsSync as any).mockReturnValue(true);
73
- (fs.readFileSync as any).mockReturnValue(JSON.stringify(mockTranslations));
74
- (path.join as any).mockImplementation((...args: any) => args.join('/'));
76
+ // 重置 import 模拟
77
+ vi.doMock('@/../locales/en-US/common.json', async () => {
78
+ return mockTranslations;
79
+ });
75
80
  });
76
81
 
77
82
  it('should return correct translation object', async () => {
@@ -88,43 +93,58 @@ describe('translation', () => {
88
93
  expect(t('nested.key')).toBe('Nested value');
89
94
  });
90
95
 
91
- it('should return key if translation is not found', async () => {
96
+ it('should handle multiple parameters in translation string', async () => {
97
+ // 模拟多参数翻译
98
+ vi.doMock('@/../locales/en-US/common.json', async () => ({
99
+ multiParam: 'Hello {{name}}, you have {{count}} messages',
100
+ }));
101
+
92
102
  const { t } = await translation('common', 'en-US');
93
- expect(t('nonexistent.key')).toBe('nonexistent.key');
103
+ expect(t('multiParam', { name: 'John', count: '5' })).toBe('Hello John, you have 5 messages');
94
104
  });
95
105
 
96
- it('should use fallback language if specified locale file does not exist', async () => {
97
- (fs.existsSync as any).mockReturnValueOnce(false);
98
- await translation('common', 'nonexistent-LANG');
99
- expect(fs.readFileSync).toHaveBeenCalledWith(
100
- expect.stringContaining(`/${DEFAULT_LANG}/common.json`),
101
- 'utf8',
102
- );
106
+ it('should handle different namespaces', async () => {
107
+ // 模拟另一个命名空间
108
+ vi.doMock('@/../locales/en-US/chat.json', async () => ({
109
+ welcome: 'Welcome to the chat',
110
+ }));
111
+
112
+ const { t } = await translation('chat', 'en-US');
113
+ expect(t('welcome')).toBe('Welcome to the chat');
103
114
  });
104
115
 
105
- it('should use zh-CN in dev mode when fallback is needed', async () => {
106
- (fs.existsSync as any).mockReturnValueOnce(false);
107
- (env.isDev as unknown as boolean) = true;
108
- await translation('common', 'nonexistent-LANG');
109
- expect(fs.readFileSync).toHaveBeenCalledWith(
110
- expect.stringContaining('/zh-CN/common.json'),
111
- 'utf8',
112
- );
116
+ it('should handle deep nested objects in translations', async () => {
117
+ // 模拟深层嵌套对象
118
+ vi.doMock('@/../locales/en-US/common.json', async () => ({
119
+ very: {
120
+ deeply: {
121
+ nested: {
122
+ key: 'Found the nested value',
123
+ },
124
+ },
125
+ },
126
+ }));
127
+
128
+ const { t } = await translation('common', 'en-US');
129
+ expect(t('very.deeply.nested.key')).toBe('Found the nested value');
113
130
  });
114
131
 
115
- it('should handle file reading errors', async () => {
116
- const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
117
- (fs.readFileSync as any).mockImplementation(() => {
118
- throw new Error('File read error');
119
- });
132
+ it('should handle empty parameters object', async () => {
133
+ vi.doMock('@/../locales/en-US/common.json', async () => ({
134
+ simpleText: 'Just a simple text',
135
+ }));
120
136
 
121
- const result = await translation('common', 'en-US');
122
- expect(result.t('any.key')).toBe('any.key');
123
- expect(consoleErrorSpy).toHaveBeenCalledWith(
124
- 'Error while reading translation file',
125
- expect.any(Error),
126
- );
137
+ const { t } = await translation('common', 'en-US');
138
+ expect(t('simpleText', {})).toBe('Just a simple text');
139
+ });
127
140
 
128
- consoleErrorSpy.mockRestore();
141
+ it('should handle missing parameters in translation string', async () => {
142
+ vi.doMock('@/../locales/en-US/common.json', async () => ({
143
+ withParam: 'Text with {{param}}',
144
+ }));
145
+
146
+ const { t } = await translation('common', 'en-US');
147
+ // 当缺少参数时应保留占位符
148
+ expect(t('withParam')).toBe('Text with {{param}}');
129
149
  });
130
150
  });