@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.
- package/.github/workflows/release.yml +21 -2
- package/.github/workflows/sync.yml +1 -1
- package/.github/workflows/test.yml +35 -4
- package/CHANGELOG.md +25 -0
- package/LICENSE +38 -21
- package/codecov.yml +11 -0
- package/drizzle.config.ts +29 -0
- package/next.config.mjs +3 -0
- package/package.json +24 -4
- package/scripts/migrateServerDB/index.ts +30 -0
- package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +2 -1
- package/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +95 -88
- package/src/app/(main)/chat/settings/features/HeaderContent.tsx +37 -31
- package/src/app/api/webhooks/clerk/__tests__/fixtures/createUser.json +73 -0
- package/src/app/api/webhooks/clerk/route.ts +159 -0
- package/src/app/api/webhooks/clerk/validateRequest.ts +22 -0
- package/src/app/trpc/edge/[trpc]/route.ts +1 -1
- package/src/app/trpc/lambda/[trpc]/route.ts +26 -0
- package/src/config/auth.ts +2 -0
- package/src/config/db.ts +13 -1
- package/src/database/server/core/db.ts +44 -0
- package/src/database/server/core/dbForTest.ts +45 -0
- package/src/database/server/index.ts +1 -0
- package/src/database/server/migrations/0000_init.sql +439 -0
- package/src/database/server/migrations/0001_add_client_id.sql +9 -0
- package/src/database/server/migrations/0002_amusing_puma.sql +9 -0
- package/src/database/server/migrations/meta/0000_snapshot.json +1583 -0
- package/src/database/server/migrations/meta/0001_snapshot.json +1636 -0
- package/src/database/server/migrations/meta/0002_snapshot.json +1630 -0
- package/src/database/server/migrations/meta/_journal.json +27 -0
- package/src/database/server/models/__tests__/file.test.ts +140 -0
- package/src/database/server/models/__tests__/message.test.ts +847 -0
- package/src/database/server/models/__tests__/plugin.test.ts +172 -0
- package/src/database/server/models/__tests__/session.test.ts +595 -0
- package/src/database/server/models/__tests__/topic.test.ts +623 -0
- package/src/database/server/models/__tests__/user.test.ts +173 -0
- package/src/database/server/models/_template.ts +44 -0
- package/src/database/server/models/file.ts +51 -0
- package/src/database/server/models/message.ts +378 -0
- package/src/database/server/models/plugin.ts +63 -0
- package/src/database/server/models/session.ts +290 -0
- package/src/database/server/models/sessionGroup.ts +69 -0
- package/src/database/server/models/topic.ts +265 -0
- package/src/database/server/models/user.ts +138 -0
- package/src/database/server/modules/DataImporter/__tests__/fixtures/messages.json +1101 -0
- package/src/database/server/modules/DataImporter/__tests__/index.test.ts +954 -0
- package/src/database/server/modules/DataImporter/index.ts +333 -0
- package/src/database/server/schemas/_id.ts +15 -0
- package/src/database/server/schemas/lobechat.ts +601 -0
- package/src/database/server/utils/idGenerator.test.ts +39 -0
- package/src/database/server/utils/idGenerator.ts +26 -0
- package/src/features/User/UserPanel/useMenu.tsx +43 -37
- package/src/libs/trpc/client.ts +52 -3
- package/src/server/files/s3.ts +21 -1
- package/src/server/keyVaultsEncrypt/index.test.ts +62 -0
- package/src/server/keyVaultsEncrypt/index.ts +93 -0
- package/src/server/mock.ts +1 -1
- package/src/server/routers/{index.ts → edge/index.ts} +3 -3
- package/src/server/routers/lambda/file.ts +49 -0
- package/src/server/routers/lambda/importer.ts +54 -0
- package/src/server/routers/lambda/index.ts +28 -0
- package/src/server/routers/lambda/message.ts +165 -0
- package/src/server/routers/lambda/plugin.ts +100 -0
- package/src/server/routers/lambda/session.ts +194 -0
- package/src/server/routers/lambda/sessionGroup.ts +77 -0
- package/src/server/routers/lambda/topic.ts +134 -0
- package/src/server/routers/lambda/user.ts +57 -0
- package/src/services/file/index.ts +4 -7
- package/src/services/file/server.ts +45 -0
- package/src/services/import/index.ts +4 -1
- package/src/services/import/server.ts +115 -0
- package/src/services/message/index.ts +4 -8
- package/src/services/message/server.ts +93 -0
- package/src/services/plugin/index.ts +4 -9
- package/src/services/plugin/server.ts +46 -0
- package/src/services/session/index.ts +4 -8
- package/src/services/session/server.ts +148 -0
- package/src/services/topic/index.ts +4 -9
- package/src/services/topic/server.ts +68 -0
- package/src/services/user/index.ts +4 -9
- package/src/services/user/server.ts +28 -0
- package/tests/setup-db.ts +7 -0
- package/vitest.config.ts +2 -1
- 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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
key: 'import',
|
|
122
|
-
label: <DataImporter>{t('import')}</DataImporter>,
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
children: [
|
|
120
|
+
const data = !isLogin
|
|
121
|
+
? []
|
|
122
|
+
: ([
|
|
126
123
|
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
...
|
|
224
|
+
...data,
|
|
219
225
|
...helps,
|
|
220
226
|
].filter(Boolean) as MenuProps['items'];
|
|
221
227
|
|
package/src/libs/trpc/client.ts
CHANGED
|
@@ -1,16 +1,65 @@
|
|
|
1
1
|
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
|
2
2
|
import superjson from 'superjson';
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
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 () =>
|
|
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
|
+
});
|
package/src/server/files/s3.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
+
}
|
package/src/server/mock.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* This file contains the root router of
|
|
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 './
|
|
7
|
-
import { uploadRouter } from './
|
|
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;
|