@lobehub/chat 0.162.25 → 0.163.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 +25 -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
@@ -1,5 +1,6 @@
1
1
  import { ActionIcon, DiscordIcon, Icon } from '@lobehub/ui';
2
2
  import { Badge } from 'antd';
3
+ import { ItemType } from 'antd/es/menu/interface';
3
4
  import {
4
5
  Book,
5
6
  CircleUserRound,
@@ -21,6 +22,7 @@ import urlJoin from 'url-join';
21
22
 
22
23
  import type { MenuProps } from '@/components/Menu';
23
24
  import { DISCORD, DOCUMENTS, EMAIL_SUPPORT, GITHUB_ISSUES, mailTo } from '@/const/url';
25
+ import { isServerMode } from '@/const/version';
24
26
  import DataImporter from '@/features/DataImporter';
25
27
  import { useOpenSettings } from '@/hooks/useInterceptingRoutes';
26
28
  import { usePWAInstall } from '@/hooks/usePWAInstall';
@@ -115,46 +117,50 @@ export const useMenu = () => {
115
117
  },
116
118
  ];
117
119
 
118
- const data: MenuProps['items'] = [
119
- {
120
- icon: <Icon icon={HardDriveUpload} />,
121
- key: 'import',
122
- label: <DataImporter>{t('import')}</DataImporter>,
123
- },
124
- {
125
- children: [
120
+ const data = !isLogin
121
+ ? []
122
+ : ([
126
123
  {
127
- key: 'allAgent',
128
- label: t('exportType.allAgent'),
129
- onClick: configService.exportAgents,
130
- },
131
- {
132
- key: 'allAgentWithMessage',
133
- label: t('exportType.allAgentWithMessage'),
134
- onClick: configService.exportSessions,
135
- },
136
- {
137
- key: 'globalSetting',
138
- label: t('exportType.globalSetting'),
139
- onClick: configService.exportSettings,
124
+ icon: <Icon icon={HardDriveDownload} />,
125
+ key: 'import',
126
+ label: <DataImporter>{t('import')}</DataImporter>,
140
127
  },
128
+ isServerMode
129
+ ? null
130
+ : {
131
+ children: [
132
+ {
133
+ key: 'allAgent',
134
+ label: t('exportType.allAgent'),
135
+ onClick: configService.exportAgents,
136
+ },
137
+ {
138
+ key: 'allAgentWithMessage',
139
+ label: t('exportType.allAgentWithMessage'),
140
+ onClick: configService.exportSessions,
141
+ },
142
+ {
143
+ key: 'globalSetting',
144
+ label: t('exportType.globalSetting'),
145
+ onClick: configService.exportSettings,
146
+ },
147
+ {
148
+ type: 'divider',
149
+ },
150
+ {
151
+ key: 'all',
152
+ label: t('exportType.all'),
153
+ onClick: configService.exportAll,
154
+ },
155
+ ],
156
+ icon: <Icon icon={HardDriveUpload} />,
157
+ key: 'export',
158
+ label: t('export'),
159
+ },
141
160
  {
142
161
  type: 'divider',
143
162
  },
144
- {
145
- key: 'all',
146
- label: t('exportType.all'),
147
- onClick: configService.exportAll,
148
- },
149
- ],
150
- icon: <Icon icon={HardDriveDownload} />,
151
- key: 'export',
152
- label: t('export'),
153
- },
154
- {
155
- type: 'divider',
156
- },
157
- ];
163
+ ].filter(Boolean) as ItemType[]);
158
164
 
159
165
  const helps: MenuProps['items'] = [
160
166
  {
@@ -209,13 +215,13 @@ export const useMenu = () => {
209
215
  {
210
216
  type: 'divider',
211
217
  },
212
- ...(isLoginWithClerk ? profile : []),
213
218
  ...(isLogin ? settings : []),
219
+ ...(isLoginWithClerk ? profile : []),
214
220
  /* ↓ cloud slot ↓ */
215
221
 
216
222
  /* ↑ cloud slot ↑ */
217
223
  ...(canInstall ? pwa : []),
218
- ...(isLogin ? data : []),
224
+ ...data,
219
225
  ...helps,
220
226
  ].filter(Boolean) as MenuProps['items'];
221
227
 
@@ -1,16 +1,65 @@
1
1
  import { createTRPCClient, httpBatchLink } from '@trpc/client';
2
2
  import superjson from 'superjson';
3
3
 
4
- import type { EdgeRouter } from '@/server/routers';
5
- import { createHeaderWithAuth } from '@/services/_auth';
4
+ import { fetchErrorNotification } from '@/components/FetchErrorNotification';
5
+ import type { EdgeRouter } from '@/server/routers/edge';
6
+ import type { LambdaRouter } from '@/server/routers/lambda';
6
7
  import { withBasePath } from '@/utils/basePath';
7
8
 
8
9
  export const edgeClient = createTRPCClient<EdgeRouter>({
9
10
  links: [
10
11
  httpBatchLink({
11
- headers: async () => createHeaderWithAuth(),
12
+ headers: async () => {
13
+ // dynamic import to avoid circular dependency
14
+ const { createHeaderWithAuth } = await import('@/services/_auth');
15
+
16
+ return createHeaderWithAuth();
17
+ },
12
18
  transformer: superjson,
13
19
  url: withBasePath('/trpc/edge'),
14
20
  }),
15
21
  ],
16
22
  });
23
+
24
+ export type ErrorResponse = ErrorItem[];
25
+
26
+ export interface ErrorItem {
27
+ error: {
28
+ json: {
29
+ code: number;
30
+ data: Data;
31
+ message: string;
32
+ };
33
+ };
34
+ }
35
+
36
+ export interface Data {
37
+ code: string;
38
+ httpStatus: number;
39
+ path: string;
40
+ stack: string;
41
+ }
42
+
43
+ export const lambdaClient = createTRPCClient<LambdaRouter>({
44
+ links: [
45
+ httpBatchLink({
46
+ fetch: async (input, init) => {
47
+ const response = await fetch(input, init);
48
+ if (response.ok) return response;
49
+
50
+ const errorRes: ErrorResponse = await response.clone().json();
51
+
52
+ errorRes.forEach((item) => {
53
+ const errorData = item.error.json;
54
+
55
+ const status = errorData.data.httpStatus;
56
+ fetchErrorNotification.error({ errorMessage: errorData.message, status });
57
+ });
58
+
59
+ return response;
60
+ },
61
+ transformer: superjson,
62
+ url: '/trpc/lambda',
63
+ }),
64
+ ],
65
+ });
@@ -1,4 +1,9 @@
1
- import { ListObjectsCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
1
+ import {
2
+ GetObjectCommand,
3
+ ListObjectsCommand,
4
+ PutObjectCommand,
5
+ S3Client,
6
+ } from '@aws-sdk/client-s3';
2
7
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
3
8
  import { z } from 'zod';
4
9
 
@@ -46,6 +51,21 @@ export class S3 {
46
51
  return listFileSchema.parse(res.Contents);
47
52
  }
48
53
 
54
+ public async getFileContent(key: string): Promise<string> {
55
+ const command = new GetObjectCommand({
56
+ Bucket: this.bucket,
57
+ Key: key,
58
+ });
59
+
60
+ const response = await this.client.send(command);
61
+
62
+ if (!response.Body) {
63
+ throw new Error(`No body in response with ${key}`);
64
+ }
65
+
66
+ return response.Body.transformToString();
67
+ }
68
+
49
69
  public async createPreSignedUrl(key: string): Promise<string> {
50
70
  const command = new PutObjectCommand({
51
71
  ACL: 'public-read',
@@ -0,0 +1,62 @@
1
+ // @vitest-environment node
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+
4
+ import { KeyVaultsGateKeeper } from './index';
5
+
6
+ describe('KeyVaultsGateKeeper', () => {
7
+ let gateKeeper: KeyVaultsGateKeeper;
8
+
9
+ beforeEach(async () => {
10
+ process.env.KEY_VAULTS_SECRET = 'Q10pwdq00KXUu9R+c8A8p4PSlIRWi7KwgUophBtkHVk=';
11
+ // 在每个测试用例运行前初始化 KeyVaultsGateKeeper 实例
12
+ gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
13
+ });
14
+
15
+ it('should encrypt and decrypt data correctly', async () => {
16
+ const originalData = 'sensitive user data';
17
+
18
+ // 加密数据
19
+ const encryptedData = await gateKeeper.encrypt(originalData);
20
+
21
+ // 解密数据
22
+ const decryptionResult = await gateKeeper.decrypt(encryptedData);
23
+
24
+ // 断言解密后的明文与原始数据相同
25
+ expect(decryptionResult.plaintext).toBe(originalData);
26
+ // 断言解密是真实的(通过认证)
27
+ expect(decryptionResult.wasAuthentic).toBe(true);
28
+ });
29
+
30
+ it('should return empty plaintext and false authenticity for invalid encrypted data', async () => {
31
+ const invalidEncryptedData = 'invalid:encrypted:data';
32
+
33
+ // 尝试解密无效的加密数据
34
+ const decryptionResult = await gateKeeper.decrypt(invalidEncryptedData);
35
+
36
+ // 断言解密后的明文为空字符串
37
+ expect(decryptionResult.plaintext).toBe('');
38
+ // 断言解密是不真实的(未通过认证)
39
+ expect(decryptionResult.wasAuthentic).toBe(false);
40
+ });
41
+
42
+ it('should throw an error if KEY_VAULTS_SECRET is not set', async () => {
43
+ // 将 KEY_VAULTS_SECRET 设为 undefined
44
+ const originalSecretKey = process.env.KEY_VAULTS_SECRET;
45
+ process.env.KEY_VAULTS_SECRET = '';
46
+
47
+ // 断言在 KEY_VAULTS_SECRET 未设置时会抛出错误
48
+ try {
49
+ await KeyVaultsGateKeeper.initWithEnvKey();
50
+ } catch (e) {
51
+ expect(e).toEqual(
52
+ Error(` \`KEY_VAULTS_SECRET\` is not set, please set it in your environment variables.
53
+
54
+ If you don't have it, please run \`openssl rand -base64 32\` to create one.
55
+ `),
56
+ );
57
+ }
58
+
59
+ // 恢复 KEY_VAULTS_SECRET 的原始值
60
+ process.env.KEY_VAULTS_SECRET = originalSecretKey;
61
+ });
62
+ });
@@ -0,0 +1,93 @@
1
+ import { getServerDBConfig } from '@/config/db';
2
+
3
+ interface DecryptionResult {
4
+ plaintext: string;
5
+ wasAuthentic: boolean;
6
+ }
7
+
8
+ export class KeyVaultsGateKeeper {
9
+ private aesKey: CryptoKey;
10
+
11
+ constructor(aesKey: CryptoKey) {
12
+ this.aesKey = aesKey;
13
+ }
14
+
15
+ static initWithEnvKey = async () => {
16
+ const { KEY_VAULTS_SECRET } = getServerDBConfig();
17
+ if (!KEY_VAULTS_SECRET)
18
+ throw new Error(` \`KEY_VAULTS_SECRET\` is not set, please set it in your environment variables.
19
+
20
+ If you don't have it, please run \`openssl rand -base64 32\` to create one.
21
+ `);
22
+
23
+ const rawKey = Buffer.from(KEY_VAULTS_SECRET, 'base64'); // 确保密钥是32字节(256位)
24
+ const aesKey = await crypto.subtle.importKey(
25
+ 'raw',
26
+ rawKey,
27
+ { length: 256, name: 'AES-GCM' },
28
+ false,
29
+ ['encrypt', 'decrypt'],
30
+ );
31
+ return new KeyVaultsGateKeeper(aesKey);
32
+ };
33
+
34
+ /**
35
+ * encrypt user private data
36
+ */
37
+ encrypt = async (keyVault: string): Promise<string> => {
38
+ const iv = crypto.getRandomValues(new Uint8Array(12)); // 对于GCM,推荐使用12字节的IV
39
+ const encodedKeyVault = new TextEncoder().encode(keyVault);
40
+
41
+ const encryptedData = await crypto.subtle.encrypt(
42
+ {
43
+ iv: iv,
44
+ name: 'AES-GCM',
45
+ },
46
+ this.aesKey,
47
+ encodedKeyVault,
48
+ );
49
+
50
+ const buffer = Buffer.from(encryptedData);
51
+ const authTag = buffer.slice(-16); // 认证标签在加密数据的最后16字节
52
+ const encrypted = buffer.slice(0, -16); // 剩下的是加密数据
53
+
54
+ return `${Buffer.from(iv).toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
55
+ };
56
+
57
+ // 假设密钥和加密数据是从外部获取的
58
+ decrypt = async (encryptedData: string): Promise<DecryptionResult> => {
59
+ const parts = encryptedData.split(':');
60
+ if (parts.length !== 3) {
61
+ throw new Error('Invalid encrypted data format');
62
+ }
63
+
64
+ const iv = Buffer.from(parts[0], 'hex');
65
+ const authTag = Buffer.from(parts[1], 'hex');
66
+ const encrypted = Buffer.from(parts[2], 'hex');
67
+
68
+ // 合并加密数据和认证标签
69
+ const combined = Buffer.concat([encrypted, authTag]);
70
+
71
+ try {
72
+ const decryptedBuffer = await crypto.subtle.decrypt(
73
+ {
74
+ iv: iv,
75
+ name: 'AES-GCM',
76
+ },
77
+ this.aesKey,
78
+ combined,
79
+ );
80
+
81
+ const decrypted = new TextDecoder().decode(decryptedBuffer);
82
+ return {
83
+ plaintext: decrypted,
84
+ wasAuthentic: true,
85
+ };
86
+ } catch {
87
+ return {
88
+ plaintext: '',
89
+ wasAuthentic: false,
90
+ };
91
+ }
92
+ };
93
+ }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  import { createCallerFactory } from '@/libs/trpc';
5
5
 
6
- import { edgeRouter } from './routers';
6
+ import { edgeRouter } from './routers/edge';
7
7
 
8
8
  export const createCaller = createCallerFactory(edgeRouter);
@@ -1,10 +1,10 @@
1
1
  /**
2
- * This file contains the root router of lobe chat tRPC-backend
2
+ * This file contains the root router of Lobe Chat tRPC-backend
3
3
  */
4
4
  import { publicProcedure, router } from '@/libs/trpc';
5
5
 
6
- import { configRouter } from './edge/config';
7
- import { uploadRouter } from './edge/upload';
6
+ import { configRouter } from './config';
7
+ import { uploadRouter } from './upload';
8
8
 
9
9
  export const edgeRouter = router({
10
10
  config: configRouter,
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+
3
+ import { FileModel } from '@/database/server/models/file';
4
+ import { authedProcedure, router } from '@/libs/trpc';
5
+ import { UploadFileSchema } from '@/types/files';
6
+
7
+ const fileProcedure = authedProcedure.use(async (opts) => {
8
+ const { ctx } = opts;
9
+
10
+ return opts.next({
11
+ ctx: { fileModel: new FileModel(ctx.userId) },
12
+ });
13
+ });
14
+
15
+ export const fileRouter = router({
16
+ createFile: fileProcedure
17
+ .input(
18
+ UploadFileSchema.omit({ data: true, saveMode: true, url: true }).extend({ url: z.string() }),
19
+ )
20
+ .mutation(async ({ ctx, input }) => {
21
+ return ctx.fileModel.create({
22
+ fileType: input.fileType,
23
+ metadata: input.metadata,
24
+ name: input.name,
25
+ size: input.size,
26
+ url: input.url,
27
+ });
28
+ }),
29
+
30
+ findById: fileProcedure
31
+ .input(
32
+ z.object({
33
+ id: z.string(),
34
+ }),
35
+ )
36
+ .query(async ({ ctx, input }) => {
37
+ return ctx.fileModel.findById(input.id);
38
+ }),
39
+
40
+ removeAllFiles: fileProcedure.mutation(async ({ ctx }) => {
41
+ return ctx.fileModel.clear();
42
+ }),
43
+
44
+ removeFile: fileProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
45
+ return ctx.fileModel.delete(input.id);
46
+ }),
47
+ });
48
+
49
+ export type FileRouter = typeof fileRouter;
@@ -0,0 +1,54 @@
1
+ // import urlJoin from 'url-join';
2
+ import { TRPCError } from '@trpc/server';
3
+ import { z } from 'zod';
4
+
5
+ // import { fileEnv } from '@/config/file';
6
+ import { DataImporter } from '@/database/server/modules/DataImporter';
7
+ import { authedProcedure, router } from '@/libs/trpc';
8
+ import { S3 } from '@/server/files/s3';
9
+ import { ImportResults, ImporterEntryData } from '@/types/importer';
10
+
11
+ export const importerRouter = router({
12
+ importByFile: authedProcedure
13
+ .input(z.object({ pathname: z.string() }))
14
+ .mutation(async ({ input, ctx }): Promise<ImportResults> => {
15
+ let data: ImporterEntryData | undefined;
16
+
17
+ try {
18
+ const s3 = new S3();
19
+ const dataStr = await s3.getFileContent(input.pathname);
20
+ data = JSON.parse(dataStr);
21
+ } catch {
22
+ data = undefined;
23
+ }
24
+
25
+ if (!data) {
26
+ throw new TRPCError({
27
+ code: 'BAD_REQUEST',
28
+ message: `Failed to read file at ${input.pathname}`,
29
+ });
30
+ }
31
+
32
+ const dataImporter = new DataImporter(ctx.userId);
33
+
34
+ return dataImporter.importData(data);
35
+ }),
36
+
37
+ importByPost: authedProcedure
38
+ .input(
39
+ z.object({
40
+ data: z.object({
41
+ messages: z.array(z.any()).optional(),
42
+ sessionGroups: z.array(z.any()).optional(),
43
+ sessions: z.array(z.any()).optional(),
44
+ topics: z.array(z.any()).optional(),
45
+ version: z.number(),
46
+ }),
47
+ }),
48
+ )
49
+ .mutation(async ({ input, ctx }): Promise<ImportResults> => {
50
+ const dataImporter = new DataImporter(ctx.userId);
51
+
52
+ return dataImporter.importData(input.data);
53
+ }),
54
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * This file contains the root router of Lobe Chat tRPC-backend
3
+ */
4
+ import { publicProcedure, router } from '@/libs/trpc';
5
+
6
+ // router that connect to db
7
+ import { fileRouter } from './file';
8
+ import { importerRouter } from './importer';
9
+ import { messageRouter } from './message';
10
+ import { pluginRouter } from './plugin';
11
+ import { sessionRouter } from './session';
12
+ import { sessionGroupRouter } from './sessionGroup';
13
+ import { topicRouter } from './topic';
14
+ import { userRouter } from './user';
15
+
16
+ export const lambdaRouter = router({
17
+ file: fileRouter,
18
+ healthcheck: publicProcedure.query(() => "i'm live!"),
19
+ importer: importerRouter,
20
+ message: messageRouter,
21
+ plugin: pluginRouter,
22
+ session: sessionRouter,
23
+ sessionGroup: sessionGroupRouter,
24
+ topic: topicRouter,
25
+ user: userRouter,
26
+ });
27
+
28
+ export type LambdaRouter = typeof lambdaRouter;
@@ -0,0 +1,165 @@
1
+ import { z } from 'zod';
2
+
3
+ import { MessageModel } from '@/database/server/models/message';
4
+ import { authedProcedure, publicProcedure, router } from '@/libs/trpc';
5
+ import { ChatMessage } from '@/types/message';
6
+ import { BatchTaskResult } from '@/types/service';
7
+
8
+ type ChatMessageList = ChatMessage[];
9
+
10
+ const messageProcedure = authedProcedure.use(async (opts) => {
11
+ const { ctx } = opts;
12
+
13
+ return opts.next({
14
+ ctx: { messageModel: new MessageModel(ctx.userId) },
15
+ });
16
+ });
17
+
18
+ export const messageRouter = router({
19
+ batchCreateMessages: messageProcedure
20
+ .input(z.array(z.any()))
21
+ .mutation(async ({ input, ctx }): Promise<BatchTaskResult> => {
22
+ const data = await ctx.messageModel.batchCreate(input);
23
+
24
+ return { added: data.rowCount as number, ids: [], skips: [], success: true };
25
+ }),
26
+
27
+ count: messageProcedure.query(async ({ ctx }) => {
28
+ return ctx.messageModel.count();
29
+ }),
30
+ countToday: messageProcedure.query(async ({ ctx }) => {
31
+ return ctx.messageModel.countToday();
32
+ }),
33
+
34
+ createMessage: messageProcedure
35
+ .input(z.object({}).passthrough().partial())
36
+ .mutation(async ({ input, ctx }) => {
37
+ const data = await ctx.messageModel.create(input as any);
38
+
39
+ return data.id;
40
+ }),
41
+
42
+ getAllMessages: messageProcedure.query(async ({ ctx }): Promise<ChatMessageList> => {
43
+ return ctx.messageModel.queryAll();
44
+ }),
45
+
46
+ getAllMessagesInSession: messageProcedure
47
+ .input(
48
+ z.object({
49
+ sessionId: z.string().nullable().optional(),
50
+ }),
51
+ )
52
+ .query(async ({ ctx, input }): Promise<ChatMessageList> => {
53
+ return ctx.messageModel.queryBySessionId(input.sessionId);
54
+ }),
55
+
56
+ getMessages: publicProcedure
57
+ .input(
58
+ z.object({
59
+ current: z.number().optional(),
60
+ pageSize: z.number().optional(),
61
+ sessionId: z.string().nullable().optional(),
62
+ topicId: z.string().nullable().optional(),
63
+ }),
64
+ )
65
+ .query(async ({ input, ctx }) => {
66
+ if (!ctx.userId) return [];
67
+
68
+ const messageModel = new MessageModel(ctx.userId);
69
+
70
+ return messageModel.query(input);
71
+ }),
72
+
73
+ removeAllMessages: messageProcedure.mutation(async ({ ctx }) => {
74
+ return ctx.messageModel.deleteAllMessages();
75
+ }),
76
+
77
+ removeMessage: messageProcedure
78
+ .input(z.object({ id: z.string() }))
79
+ .mutation(async ({ input, ctx }) => {
80
+ return ctx.messageModel.deleteMessage(input.id);
81
+ }),
82
+
83
+ removeMessages: messageProcedure
84
+ .input(
85
+ z.object({
86
+ sessionId: z.string().nullable().optional(),
87
+ topicId: z.string().nullable().optional(),
88
+ }),
89
+ )
90
+ .mutation(async ({ input, ctx }) => {
91
+ return ctx.messageModel.deleteMessages(input.sessionId, input.topicId);
92
+ }),
93
+
94
+ searchMessages: messageProcedure
95
+ .input(z.object({ keywords: z.string() }))
96
+ .query(async ({ input, ctx }) => {
97
+ return ctx.messageModel.queryByKeyword(input.keywords);
98
+ }),
99
+
100
+ update: messageProcedure
101
+ .input(
102
+ z.object({
103
+ id: z.string(),
104
+ value: z.object({}).passthrough().partial(),
105
+ }),
106
+ )
107
+ .mutation(async ({ input, ctx }) => {
108
+ return ctx.messageModel.update(input.id, input.value);
109
+ }),
110
+
111
+ updatePluginState: messageProcedure
112
+ .input(
113
+ z.object({
114
+ id: z.string(),
115
+ value: z.object({}).passthrough(),
116
+ }),
117
+ )
118
+ .mutation(async ({ input, ctx }) => {
119
+ return ctx.messageModel.updatePluginState(input.id, input.value);
120
+ }),
121
+
122
+ updateTTS: messageProcedure
123
+ .input(
124
+ z.object({
125
+ id: z.string(),
126
+ value: z
127
+ .object({
128
+ contentMd5: z.string().optional(),
129
+ fileId: z.string().optional(),
130
+ voice: z.string().optional(),
131
+ })
132
+ .or(z.literal(false)),
133
+ }),
134
+ )
135
+ .mutation(async ({ input, ctx }) => {
136
+ if (input.value === false) {
137
+ return ctx.messageModel.deleteMessageTTS(input.id);
138
+ }
139
+
140
+ return ctx.messageModel.updateTTS(input.id, input.value);
141
+ }),
142
+
143
+ updateTranslate: messageProcedure
144
+ .input(
145
+ z.object({
146
+ id: z.string(),
147
+ value: z
148
+ .object({
149
+ content: z.string().optional(),
150
+ from: z.string().optional(),
151
+ to: z.string(),
152
+ })
153
+ .or(z.literal(false)),
154
+ }),
155
+ )
156
+ .mutation(async ({ input, ctx }) => {
157
+ if (input.value === false) {
158
+ return ctx.messageModel.deleteMessageTranslate(input.id);
159
+ }
160
+
161
+ return ctx.messageModel.updateTranslate(input.id, input.value);
162
+ }),
163
+ });
164
+
165
+ export type MessageRouter = typeof messageRouter;