@lobehub/chat 1.12.10 → 1.12.12
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 +3 -2
- package/.github/workflows/test.yml +2 -1
- package/CHANGELOG.md +42 -0
- package/Dockerfile.database +1 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/self-hosting/advanced/auth.mdx +4 -1
- package/docs/self-hosting/advanced/s3.mdx +6 -3
- package/docs/self-hosting/advanced/s3.zh-CN.mdx +0 -2
- package/docs/self-hosting/advanced/upstream-sync.mdx +12 -0
- package/docs/self-hosting/advanced/upstream-sync.zh-CN.mdx +12 -0
- package/docs/self-hosting/server-database/vercel.mdx +6 -0
- package/docs/self-hosting/server-database/vercel.zh-CN.mdx +6 -0
- package/package.json +2 -2
- package/src/config/app.ts +12 -5
- package/src/config/db.ts +4 -0
- package/src/database/server/models/__tests__/chunk.test.ts +60 -1
- package/src/database/server/models/__tests__/file.test.ts +211 -66
- package/src/database/server/models/__tests__/knowledgeBase.test.ts +128 -2
- package/src/database/server/models/__tests__/user.test.ts +36 -1
- package/src/database/server/models/chunk.ts +14 -1
- package/src/database/server/models/file.ts +4 -2
- package/src/server/routers/lambda/file.ts +5 -0
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
|
19
19
|
ports:
|
|
20
20
|
- 5432:5432
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
steps:
|
|
23
23
|
- uses: actions/checkout@v4
|
|
24
24
|
|
|
@@ -40,7 +40,8 @@ jobs:
|
|
|
40
40
|
DATABASE_DRIVER: node
|
|
41
41
|
NEXT_PUBLIC_SERVICE_MODE: server
|
|
42
42
|
KEY_VAULTS_SECRET: LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=
|
|
43
|
-
|
|
43
|
+
S3_PUBLIC_DOMAIN: https://example.com
|
|
44
|
+
APP_URL: https://home.com
|
|
44
45
|
|
|
45
46
|
- name: Test App Coverage
|
|
46
47
|
run: bun run test-app:coverage
|
|
@@ -39,7 +39,8 @@ jobs:
|
|
|
39
39
|
DATABASE_DRIVER: node
|
|
40
40
|
NEXT_PUBLIC_SERVICE_MODE: server
|
|
41
41
|
KEY_VAULTS_SECRET: LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=
|
|
42
|
-
|
|
42
|
+
S3_PUBLIC_DOMAIN: https://example.com
|
|
43
|
+
APP_URL: https://home.com
|
|
43
44
|
|
|
44
45
|
- name: Upload Server coverage to Codecov
|
|
45
46
|
uses: codecov/codecov-action@v4
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 1.12.12](https://github.com/lobehub/lobe-chat/compare/v1.12.11...v1.12.12)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-08-24**</sup>
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
<details>
|
|
12
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
13
|
+
|
|
14
|
+
</details>
|
|
15
|
+
|
|
16
|
+
<div align="right">
|
|
17
|
+
|
|
18
|
+
[](#readme-top)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
### [Version 1.12.11](https://github.com/lobehub/lobe-chat/compare/v1.12.10...v1.12.11)
|
|
23
|
+
|
|
24
|
+
<sup>Released on **2024-08-23**</sup>
|
|
25
|
+
|
|
26
|
+
#### 🐛 Bug Fixes
|
|
27
|
+
|
|
28
|
+
- **misc**: Remove orphan chunks if there is no related file.
|
|
29
|
+
|
|
30
|
+
<br/>
|
|
31
|
+
|
|
32
|
+
<details>
|
|
33
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
34
|
+
|
|
35
|
+
#### What's fixed
|
|
36
|
+
|
|
37
|
+
- **misc**: Remove orphan chunks if there is no related file, closes [#3578](https://github.com/lobehub/lobe-chat/issues/3578) ([36bcaf3](https://github.com/lobehub/lobe-chat/commit/36bcaf3))
|
|
38
|
+
|
|
39
|
+
</details>
|
|
40
|
+
|
|
41
|
+
<div align="right">
|
|
42
|
+
|
|
43
|
+
[](#readme-top)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
5
47
|
### [Version 1.12.10](https://github.com/lobehub/lobe-chat/compare/v1.12.9...v1.12.10)
|
|
6
48
|
|
|
7
49
|
<sup>Released on **2024-08-23**</sup>
|
package/Dockerfile.database
CHANGED
package/README.md
CHANGED
|
@@ -269,11 +269,11 @@ Our marketplace is not just a showcase platform but also a collaborative space.
|
|
|
269
269
|
| Recent Submits | Description |
|
|
270
270
|
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
271
271
|
| [Variable Name Conversion Expert](https://chat-preview.lobehub.com/market?agent=variable-name-conversion)<br/><sup>By **[zengyishou](https://github.com/zengyishou)** on **2024-08-21**</sup> | In software development, naming variables is a common yet relatively time-consuming task. This assistant can automatically convert Chinese variable names into English variable names that conform to camelCase, PascalCase, snake_case, kebab-case, and constant naming conventions based on specific naming rules. This not only improves code readability but also alleviates the frustration of variable naming.<br/>`software-development` `variable-naming` `chinese-to-english` `code-standards` `automatic-conversion` |
|
|
272
|
+
| [Prompt Engineering Expert](https://chat-preview.lobehub.com/market?agent=ai-prompts-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | Specializing in prompt optimization and design<br/>`prompt-engineering` `ai-interaction` `writing` `optimization` `consulting` |
|
|
272
273
|
| [Commit Message Generator](https://chat-preview.lobehub.com/market?agent=commit-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | Expert at generating precise Git commit messages<br/>`programming` `git` `commit-message` `code-review` |
|
|
273
274
|
| [RO-SCIRAW Prompt Word Expert](https://chat-preview.lobehub.com/market?agent=rosciraw)<br/><sup>By **[kirklin](https://github.com/kirklin)** on **2024-08-06**</sup> | The RO-SCIRAW framework, created by Kirk Lin, is a methodology for prompt words that provides a new paradigm for building highly precise and efficient prompt words. Please enter the information for the persona you want to create.<br/>`prompt-word-framework` |
|
|
274
|
-
| [Social Media Sage](https://chat-preview.lobehub.com/market?agent=social-media-sage)<br/><sup>By **[thedivergentai](https://github.com/thedivergentai)** on **2024-08-06**</sup> | Social Media Marketing expert crafting winning strategies for brands and empowering businesses to thrive online<br/>`social-media-marketing` `branding` `growth-strategies` |
|
|
275
275
|
|
|
276
|
-
> 📊 Total agents: [<kbd>**
|
|
276
|
+
> 📊 Total agents: [<kbd>**315**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
|
|
277
277
|
|
|
278
278
|
<!-- AGENT LIST -->
|
|
279
279
|
|
package/README.zh-CN.md
CHANGED
|
@@ -257,11 +257,11 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
|
|
257
257
|
| 最近新增 | 助手说明 |
|
|
258
258
|
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
259
259
|
| [开发变量名转换专家](https://chat-preview.lobehub.com/market?agent=variable-name-conversion)<br/><sup>By **[zengyishou](https://github.com/zengyishou)** on **2024-08-21**</sup> | 在软件开发过程中,命名变量是一项常见却相对耗时的任务。本助手能够根据特定的命名规则自动将中文变量名转换为符合小驼峰、大驼峰、下划线、横线以及常量命名规范的英文变量名。这不仅提高了代码的可读性,还解决了变量命名的苦恼。<br/>`软件开发` `变量命名` `中文转英文` `代码规范` `自动转换` |
|
|
260
|
+
| [提示工程专家](https://chat-preview.lobehub.com/market?agent=ai-prompts-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | 专精 Prompt 优化与设计<br/>`提示工程` `ai交互` `写作` `优化` `咨询` |
|
|
260
261
|
| [提交信息生成器](https://chat-preview.lobehub.com/market?agent=commit-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | 擅长生成精准的 Git 提交信息<br/>`编程` `git` `提交信息` `代码审查` |
|
|
261
262
|
| [RO-SCIRAW 提示词专家](https://chat-preview.lobehub.com/market?agent=rosciraw)<br/><sup>By **[kirklin](https://github.com/kirklin)** on **2024-08-06**</sup> | RO-SCIRAW 框架是由 Kirk Lin 开创的提示词方法论,为构建高度精确和高效的提示词提供了一个全新的范式。请输入你要创建的分身信息。<br/>`提示词框架` |
|
|
262
|
-
| [社交媒体专家](https://chat-preview.lobehub.com/market?agent=social-media-sage)<br/><sup>By **[thedivergentai](https://github.com/thedivergentai)** on **2024-08-06**</sup> | 社交媒体营销专家,为品牌制定成功策略,帮助企业在线蓬勃发展<br/>`社交媒体营销` `品牌塑造` `增长策略` |
|
|
263
263
|
|
|
264
|
-
> 📊 Total agents: [<kbd>**
|
|
264
|
+
> 📊 Total agents: [<kbd>**315**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
|
|
265
265
|
|
|
266
266
|
<!-- AGENT LIST -->
|
|
267
267
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: LobeChat Authentication Service Configuration
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Learn how to configure external authentication services using Clerk or Next
|
|
5
|
+
Auth for centralized user authorization management. Supported authentication
|
|
6
|
+
services include Auth0, Azure ID, etc.
|
|
4
7
|
tags:
|
|
5
8
|
- Authentication Service
|
|
6
9
|
- Next Auth
|
|
@@ -51,7 +51,8 @@ In this documentation, S3 refers to a compatible S3 storage solution, which supp
|
|
|
51
51
|
|
|
52
52
|
<Callout type={'warning'}>
|
|
53
53
|
Additionally, since this access domain is often a separate URL, it needs to be configured to allow cross-origin access to the site. Otherwise, cross-origin issues will occur in the browser.
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
</Callout>
|
|
55
56
|
|
|
56
57
|
### `S3_ENABLE_PATH_STYLE`
|
|
57
58
|
|
|
@@ -65,13 +66,15 @@ In this documentation, S3 refers to a compatible S3 storage solution, which supp
|
|
|
65
66
|
- path-style: `s3.example.net/mybucket/config.env`
|
|
66
67
|
- virtual-host: `mybucket.s3.example.net/config.env`
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
|
|
70
|
+
</Callout>
|
|
69
71
|
|
|
70
72
|
<Callout type={'tip'}>
|
|
71
73
|
|
|
72
74
|
Common S3 cloud service providers often default to the `virtual-host` mode, while self-deployed services like Minio default to using `path-style`. Therefore, if you use Minio as the S3 service, you need to set `S3_ENABLE_PATH_STYLE=1`.
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
|
|
77
|
+
</Callout>
|
|
75
78
|
|
|
76
79
|
</Steps>
|
|
77
80
|
|
|
@@ -73,10 +73,8 @@ S3_ENDPOINT=https://0b33a03b5c993fd2f453379dc36558e5.r2.cloudflarestorage.com
|
|
|
73
73
|
|
|
74
74
|
常见的 S3 Cloud 服务商往往默认采用 `virtual-host` 模式,而自部署服务 minio 则默认使用的是 `path-style`。 因此如果你使用了 minio 作为 S3 服务,你需要设置 `S3_ENABLE_PATH_STYLE=1` 。
|
|
75
75
|
|
|
76
|
-
|
|
77
76
|
</Callout>
|
|
78
77
|
|
|
79
|
-
|
|
80
78
|
</Steps>
|
|
81
79
|
|
|
82
80
|
## S3 配置指南
|
|
@@ -40,6 +40,18 @@ After forking the project, due to Github's limitations, you need to manually ena
|
|
|
40
40
|
src="https://github-production-user-asset-6210df.s3.amazonaws.com/17870709/266985177-7677b4ce-c348-4145-9f60-829d448d5be6.png"
|
|
41
41
|
/>
|
|
42
42
|
|
|
43
|
+
If you encounter a sync failure, you need to manually click "Update Branch" once.
|
|
44
|
+
|
|
45
|
+
<Image
|
|
46
|
+
alt="GitHub Action Sync Failure"
|
|
47
|
+
src="https://github.com/user-attachments/assets/9baacac6-5af4-460b-862d-682b76c18459"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<Image
|
|
51
|
+
alt="Manually Sync 'Update Branch'"
|
|
52
|
+
src="https://github.com/user-attachments/assets/d524c20d-306a-45bc-971b-96920b87fab4"
|
|
53
|
+
/>
|
|
54
|
+
|
|
43
55
|
## `B` Docker Deployment
|
|
44
56
|
|
|
45
57
|
Upgrading the Docker deployment version is very simple, you just need to redeploy the latest LobeChat image. Here are the commands required to perform these steps:
|
|
@@ -35,6 +35,18 @@ tags:
|
|
|
35
35
|
src="https://github-production-user-asset-6210df.s3.amazonaws.com/17870709/266985177-7677b4ce-c348-4145-9f60-829d448d5be6.png"
|
|
36
36
|
/>
|
|
37
37
|
|
|
38
|
+
如果你遇到了同步失败的情况,你需要手动重新点一次 「Update Branch」。
|
|
39
|
+
|
|
40
|
+
<Image
|
|
41
|
+
alt="GitHub Action 同步失败"
|
|
42
|
+
src="https://github.com/user-attachments/assets/9baacac6-5af4-460b-862d-682b76c18459"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<Image
|
|
46
|
+
alt="手动同步 「Update Branch」"
|
|
47
|
+
src="https://github.com/user-attachments/assets/d524c20d-306a-45bc-971b-96920b87fab4"
|
|
48
|
+
/>
|
|
49
|
+
|
|
38
50
|
## `B` Docker 部署
|
|
39
51
|
|
|
40
52
|
Docker 部署版本的升级非常简单,只需要重新部署 LobeChat 的最新镜像即可。 以下是执行这些步骤所需的指令:
|
|
@@ -109,6 +109,10 @@ KEY_VAULTS_SECRET=jgwsK28dspyVQoIf8/M3IIHl1h6LYYceSYNXeLpy6uk=
|
|
|
109
109
|
|
|
110
110
|
Make sure to add this to the Vercel environment variables as well.
|
|
111
111
|
|
|
112
|
+
### Add the `APP_URL` Environment Variable
|
|
113
|
+
|
|
114
|
+
Finally, you need to add the `APP_URL` environment variable, which specifies the URL address of the LobeChat application.
|
|
115
|
+
|
|
112
116
|
</Steps>
|
|
113
117
|
|
|
114
118
|
## 2. Configure Authentication Service
|
|
@@ -394,6 +398,8 @@ src={'https://github.com/lobehub/lobe-chat/assets/28616219/9cb5150d-6e1e-4c59-9a
|
|
|
394
398
|
For easy copying, here is a summary of the environment variables required to configure the server-side database:
|
|
395
399
|
|
|
396
400
|
```shell
|
|
401
|
+
APP_URL=https://your-project.com
|
|
402
|
+
|
|
397
403
|
# Specify the service mode as server
|
|
398
404
|
NEXT_PUBLIC_SERVICE_MODE=server
|
|
399
405
|
|
|
@@ -102,6 +102,10 @@ KEY_VAULTS_SECRET=jgwsK28dspyVQoIf8/M3IIHl1h6LYYceSYNXeLpy6uk=
|
|
|
102
102
|
|
|
103
103
|
同样需要将其添加到 Vercel 环境变量中。
|
|
104
104
|
|
|
105
|
+
### 添加 `APP_URL` 环境变量
|
|
106
|
+
|
|
107
|
+
该部分最后需要添加 APP_URL 环境变量,用于指定LobeChat 应用的 URL 地址。
|
|
108
|
+
|
|
105
109
|
</Steps>
|
|
106
110
|
|
|
107
111
|
## 二、 配置身份验证服务
|
|
@@ -375,6 +379,8 @@ S3_PUBLIC_DOMAIN=https://s3-dev.your-domain.com
|
|
|
375
379
|
为方便一键复制,在此汇总配置服务端数据库所需要的环境变量:
|
|
376
380
|
|
|
377
381
|
```shell
|
|
382
|
+
APP_URL=https://your-project.com
|
|
383
|
+
|
|
378
384
|
# 指定服务模式为 server
|
|
379
385
|
NEXT_PUBLIC_SERVICE_MODE=server
|
|
380
386
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.12",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"eslint-plugin-mdx": "^2.3.4",
|
|
261
261
|
"eslint-plugin-unused-imports": "4.0.1",
|
|
262
262
|
"fake-indexeddb": "^6.0.0",
|
|
263
|
-
"glob": "^
|
|
263
|
+
"glob": "^11.0.0",
|
|
264
264
|
"gray-matter": "^4.0.3",
|
|
265
265
|
"happy-dom": "^14.12.3",
|
|
266
266
|
"husky": "^9.1.5",
|
package/src/config/app.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { createEnv } from '@t3-oss/env-nextjs';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
+
import { isServerMode } from '@/const/version';
|
|
6
|
+
|
|
5
7
|
declare global {
|
|
6
8
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
7
9
|
namespace NodeJS {
|
|
@@ -10,14 +12,19 @@ declare global {
|
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
}
|
|
15
|
+
const isInVercel = process.env.VERCEL === '1';
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
const ACCESS_CODES = process.env.ACCESS_CODE?.split(',').filter(Boolean) || [];
|
|
16
|
-
const isInVercel = process.env.VERCEL === '1';
|
|
17
|
+
const vercelUrl = `https://${process.env.VERCEL_URL}`;
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
const APP_URL = process.env.APP_URL ? process.env.APP_URL : isInVercel ? vercelUrl : undefined;
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// only throw error in server mode and server side
|
|
22
|
+
if (typeof window === 'undefined' && isServerMode && !APP_URL) {
|
|
23
|
+
throw new Error('`APP_URL` is required in server mode');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const getAppConfig = () => {
|
|
27
|
+
const ACCESS_CODES = process.env.ACCESS_CODE?.split(',').filter(Boolean) || [];
|
|
21
28
|
|
|
22
29
|
return createEnv({
|
|
23
30
|
client: {
|
package/src/config/db.ts
CHANGED
|
@@ -11,6 +11,8 @@ export const getServerDBConfig = () => {
|
|
|
11
11
|
DATABASE_TEST_URL: process.env.DATABASE_TEST_URL,
|
|
12
12
|
DATABASE_URL: process.env.DATABASE_URL,
|
|
13
13
|
|
|
14
|
+
DISABLE_REMOVE_GLOBAL_FILE: process.env.DISABLE_REMOVE_GLOBAL_FILE === '1',
|
|
15
|
+
|
|
14
16
|
KEY_VAULTS_SECRET: process.env.KEY_VAULTS_SECRET,
|
|
15
17
|
|
|
16
18
|
NEXT_PUBLIC_ENABLED_SERVER_SERVICE: process.env.NEXT_PUBLIC_SERVICE_MODE === 'server',
|
|
@@ -20,6 +22,8 @@ export const getServerDBConfig = () => {
|
|
|
20
22
|
DATABASE_TEST_URL: z.string().optional(),
|
|
21
23
|
DATABASE_URL: z.string().optional(),
|
|
22
24
|
|
|
25
|
+
DISABLE_REMOVE_GLOBAL_FILE: z.boolean().optional(),
|
|
26
|
+
|
|
23
27
|
KEY_VAULTS_SECRET: z.string().optional(),
|
|
24
28
|
},
|
|
25
29
|
});
|
|
@@ -98,7 +98,66 @@ describe('ChunkModel', () => {
|
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
describe('deleteOrphanChunks', () => {
|
|
102
|
+
it('should delete orphaned chunks', async () => {
|
|
103
|
+
// Create orphaned chunks
|
|
104
|
+
await serverDB
|
|
105
|
+
.insert(chunks)
|
|
106
|
+
.values([
|
|
107
|
+
{ text: 'Orphan Chunk 1', userId },
|
|
108
|
+
{ text: 'Orphan Chunk 2', userId },
|
|
109
|
+
])
|
|
110
|
+
.returning();
|
|
111
|
+
|
|
112
|
+
// Create a non-orphaned chunk
|
|
113
|
+
const [nonOrphanChunk] = await serverDB
|
|
114
|
+
.insert(chunks)
|
|
115
|
+
.values([{ text: 'Non-Orphan Chunk', userId }])
|
|
116
|
+
.returning();
|
|
117
|
+
|
|
118
|
+
await serverDB.insert(fileChunks).values([{ fileId: '1', chunkId: nonOrphanChunk.id }]);
|
|
119
|
+
|
|
120
|
+
// Execute the method
|
|
121
|
+
await chunkModel.deleteOrphanChunks();
|
|
122
|
+
|
|
123
|
+
// Check if orphaned chunks are deleted
|
|
124
|
+
const remainingChunks = await serverDB.query.chunks.findMany();
|
|
125
|
+
expect(remainingChunks).toHaveLength(1);
|
|
126
|
+
expect(remainingChunks[0].id).toBe(nonOrphanChunk.id);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should not delete any chunks when there are no orphans', async () => {
|
|
130
|
+
// Create non-orphaned chunks
|
|
131
|
+
const [chunk1, chunk2] = await serverDB
|
|
132
|
+
.insert(chunks)
|
|
133
|
+
.values([
|
|
134
|
+
{ text: 'Chunk 1', userId },
|
|
135
|
+
{ text: 'Chunk 2', userId },
|
|
136
|
+
])
|
|
137
|
+
.returning();
|
|
138
|
+
|
|
139
|
+
await serverDB.insert(fileChunks).values([
|
|
140
|
+
{ fileId: '1', chunkId: chunk1.id },
|
|
141
|
+
{ fileId: '2', chunkId: chunk2.id },
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
// Execute the method
|
|
145
|
+
await chunkModel.deleteOrphanChunks();
|
|
146
|
+
|
|
147
|
+
// Check if all chunks are still present
|
|
148
|
+
const remainingChunks = await serverDB.query.chunks.findMany();
|
|
149
|
+
expect(remainingChunks).toHaveLength(2);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should not throw an error when the database is empty', async () => {
|
|
153
|
+
// Ensure the database is empty
|
|
154
|
+
await serverDB.delete(chunks);
|
|
155
|
+
await serverDB.delete(fileChunks);
|
|
156
|
+
|
|
157
|
+
// Execute the method and expect it not to throw
|
|
158
|
+
await expect(chunkModel.deleteOrphanChunks()).resolves.not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
102
161
|
|
|
103
162
|
describe('semanticSearch', () => {
|
|
104
163
|
it('should perform semantic search and return results', async () => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
|
-
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { eq, inArray } from 'drizzle-orm';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
|
+
import { getServerDBConfig, serverDBEnv } from '@/config/db';
|
|
5
6
|
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
|
6
7
|
import { FilesTabs, SortType } from '@/types/files';
|
|
7
8
|
|
|
@@ -22,6 +23,20 @@ vi.mock('@/database/server/core/db', async () => ({
|
|
|
22
23
|
},
|
|
23
24
|
}));
|
|
24
25
|
|
|
26
|
+
let DISABLE_REMOVE_GLOBAL_FILE = false;
|
|
27
|
+
|
|
28
|
+
vi.mock('@/config/db', async () => ({
|
|
29
|
+
get serverDBEnv() {
|
|
30
|
+
return {
|
|
31
|
+
get DISABLE_REMOVE_GLOBAL_FILE() {
|
|
32
|
+
return DISABLE_REMOVE_GLOBAL_FILE;
|
|
33
|
+
},
|
|
34
|
+
DATABASE_TEST_URL: process.env.DATABASE_TEST_URL,
|
|
35
|
+
DATABASE_DRIVER: 'node',
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
25
40
|
const userId = 'file-model-test-user-id';
|
|
26
41
|
const fileModel = new FileModel(userId);
|
|
27
42
|
|
|
@@ -73,18 +88,21 @@ describe('FileModel', () => {
|
|
|
73
88
|
});
|
|
74
89
|
});
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
describe('createGlobalFile', () => {
|
|
92
|
+
it('should create a global file', async () => {
|
|
93
|
+
const globalFile = {
|
|
94
|
+
hashId: 'test-hash',
|
|
95
|
+
fileType: 'text/plain',
|
|
96
|
+
size: 100,
|
|
97
|
+
url: 'https://example.com/global-file.txt',
|
|
98
|
+
metadata: { key: 'value' },
|
|
99
|
+
};
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
const result = await fileModel.createGlobalFile(globalFile);
|
|
102
|
+
expect(result[0]).toMatchObject(globalFile);
|
|
103
|
+
});
|
|
87
104
|
});
|
|
105
|
+
|
|
88
106
|
describe('checkHash', () => {
|
|
89
107
|
it('should return isExist: false for non-existent hash', async () => {
|
|
90
108
|
const result = await fileModel.checkHash('non-existent-hash');
|
|
@@ -113,58 +131,183 @@ describe('FileModel', () => {
|
|
|
113
131
|
});
|
|
114
132
|
});
|
|
115
133
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
describe('delete', () => {
|
|
135
|
+
it('should delete a file by id', async () => {
|
|
136
|
+
await fileModel.createGlobalFile({
|
|
137
|
+
hashId: '1',
|
|
138
|
+
url: 'https://example.com/file1.txt',
|
|
139
|
+
size: 100,
|
|
140
|
+
fileType: 'text/plain',
|
|
141
|
+
});
|
|
123
142
|
|
|
124
|
-
|
|
143
|
+
const { id } = await fileModel.create({
|
|
144
|
+
name: 'test-file.txt',
|
|
145
|
+
url: 'https://example.com/test-file.txt',
|
|
146
|
+
size: 100,
|
|
147
|
+
fileType: 'text/plain',
|
|
148
|
+
fileHash: '1',
|
|
149
|
+
});
|
|
125
150
|
|
|
126
|
-
|
|
127
|
-
expect(file).toBeUndefined();
|
|
128
|
-
});
|
|
151
|
+
await fileModel.delete(id);
|
|
129
152
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const file2 = await fileModel.create({
|
|
138
|
-
name: 'file2.txt',
|
|
139
|
-
url: 'https://example.com/file2.txt',
|
|
140
|
-
size: 200,
|
|
141
|
-
fileType: 'text/plain',
|
|
153
|
+
const file = await serverDB.query.files.findFirst({ where: eq(files.id, id) });
|
|
154
|
+
const globalFile = await serverDB.query.globalFiles.findFirst({
|
|
155
|
+
where: eq(globalFiles.hashId, '1'),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(file).toBeUndefined();
|
|
159
|
+
expect(globalFile).toBeUndefined();
|
|
142
160
|
});
|
|
161
|
+
it('should delete a file by id but global file not removed ', async () => {
|
|
162
|
+
DISABLE_REMOVE_GLOBAL_FILE = true;
|
|
163
|
+
await fileModel.createGlobalFile({
|
|
164
|
+
hashId: '1',
|
|
165
|
+
url: 'https://example.com/file1.txt',
|
|
166
|
+
size: 100,
|
|
167
|
+
fileType: 'text/plain',
|
|
168
|
+
});
|
|
143
169
|
|
|
144
|
-
|
|
170
|
+
const { id } = await fileModel.create({
|
|
171
|
+
name: 'test-file.txt',
|
|
172
|
+
url: 'https://example.com/test-file.txt',
|
|
173
|
+
size: 100,
|
|
174
|
+
fileType: 'text/plain',
|
|
175
|
+
fileHash: '1',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await fileModel.delete(id);
|
|
179
|
+
|
|
180
|
+
const file = await serverDB.query.files.findFirst({ where: eq(files.id, id) });
|
|
181
|
+
const globalFile = await serverDB.query.globalFiles.findFirst({
|
|
182
|
+
where: eq(globalFiles.hashId, '1'),
|
|
183
|
+
});
|
|
145
184
|
|
|
146
|
-
|
|
147
|
-
|
|
185
|
+
expect(file).toBeUndefined();
|
|
186
|
+
expect(globalFile).toBeDefined();
|
|
187
|
+
DISABLE_REMOVE_GLOBAL_FILE = false;
|
|
188
|
+
});
|
|
148
189
|
});
|
|
149
190
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
191
|
+
describe('deleteMany', () => {
|
|
192
|
+
it('should delete multiple files', async () => {
|
|
193
|
+
await fileModel.createGlobalFile({
|
|
194
|
+
hashId: '1',
|
|
195
|
+
url: 'https://example.com/file1.txt',
|
|
196
|
+
size: 100,
|
|
197
|
+
fileType: 'text/plain',
|
|
198
|
+
});
|
|
199
|
+
await fileModel.createGlobalFile({
|
|
200
|
+
hashId: '2',
|
|
201
|
+
url: 'https://example.com/file2.txt',
|
|
202
|
+
size: 200,
|
|
203
|
+
fileType: 'text/plain',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const file1 = await fileModel.create({
|
|
207
|
+
name: 'file1.txt',
|
|
208
|
+
url: 'https://example.com/file1.txt',
|
|
209
|
+
size: 100,
|
|
210
|
+
fileHash: '1',
|
|
211
|
+
fileType: 'text/plain',
|
|
212
|
+
});
|
|
213
|
+
const file2 = await fileModel.create({
|
|
214
|
+
name: 'file2.txt',
|
|
215
|
+
url: 'https://example.com/file2.txt',
|
|
216
|
+
size: 200,
|
|
217
|
+
fileType: 'text/plain',
|
|
218
|
+
fileHash: '2',
|
|
219
|
+
});
|
|
220
|
+
const globalFilesResult = await serverDB.query.globalFiles.findMany({
|
|
221
|
+
where: inArray(globalFiles.hashId, ['1', '2']),
|
|
222
|
+
});
|
|
223
|
+
expect(globalFilesResult).toHaveLength(2);
|
|
224
|
+
|
|
225
|
+
await fileModel.deleteMany([file1.id, file2.id]);
|
|
226
|
+
|
|
227
|
+
const remainingFiles = await serverDB.query.files.findMany({
|
|
228
|
+
where: eq(files.userId, userId),
|
|
229
|
+
});
|
|
230
|
+
const globalFilesResult2 = await serverDB.query.globalFiles.findMany({
|
|
231
|
+
where: inArray(
|
|
232
|
+
globalFiles.hashId,
|
|
233
|
+
remainingFiles.map((i) => i.fileHash as string),
|
|
234
|
+
),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(remainingFiles).toHaveLength(0);
|
|
238
|
+
expect(globalFilesResult2).toHaveLength(0);
|
|
156
239
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
240
|
+
it('should delete multiple files but not remove global files if DISABLE_REMOVE_GLOBAL_FILE=true', async () => {
|
|
241
|
+
DISABLE_REMOVE_GLOBAL_FILE = true;
|
|
242
|
+
await fileModel.createGlobalFile({
|
|
243
|
+
hashId: '1',
|
|
244
|
+
url: 'https://example.com/file1.txt',
|
|
245
|
+
size: 100,
|
|
246
|
+
fileType: 'text/plain',
|
|
247
|
+
});
|
|
248
|
+
await fileModel.createGlobalFile({
|
|
249
|
+
hashId: '2',
|
|
250
|
+
url: 'https://example.com/file2.txt',
|
|
251
|
+
size: 200,
|
|
252
|
+
fileType: 'text/plain',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const file1 = await fileModel.create({
|
|
256
|
+
name: 'file1.txt',
|
|
257
|
+
url: 'https://example.com/file1.txt',
|
|
258
|
+
size: 100,
|
|
259
|
+
fileType: 'text/plain',
|
|
260
|
+
fileHash: '1',
|
|
261
|
+
});
|
|
262
|
+
const file2 = await fileModel.create({
|
|
263
|
+
name: 'file2.txt',
|
|
264
|
+
url: 'https://example.com/file2.txt',
|
|
265
|
+
size: 200,
|
|
266
|
+
fileType: 'text/plain',
|
|
267
|
+
fileHash: '2',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const globalFilesResult = await serverDB.query.globalFiles.findMany({
|
|
271
|
+
where: inArray(globalFiles.hashId, ['1', '2']),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(globalFilesResult).toHaveLength(2);
|
|
275
|
+
|
|
276
|
+
await fileModel.deleteMany([file1.id, file2.id]);
|
|
277
|
+
|
|
278
|
+
const remainingFiles = await serverDB.query.files.findMany({
|
|
279
|
+
where: eq(files.userId, userId),
|
|
280
|
+
});
|
|
281
|
+
const globalFilesResult2 = await serverDB.query.globalFiles.findMany({
|
|
282
|
+
where: inArray(globalFiles.hashId, ['1', '2']),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(remainingFiles).toHaveLength(0);
|
|
286
|
+
expect(globalFilesResult2).toHaveLength(2);
|
|
287
|
+
DISABLE_REMOVE_GLOBAL_FILE = false;
|
|
162
288
|
});
|
|
289
|
+
});
|
|
163
290
|
|
|
164
|
-
|
|
291
|
+
describe('clear', () => {
|
|
292
|
+
it('should clear all files for the user', async () => {
|
|
293
|
+
await fileModel.create({
|
|
294
|
+
name: 'test-file-1.txt',
|
|
295
|
+
url: 'https://example.com/test-file-1.txt',
|
|
296
|
+
size: 100,
|
|
297
|
+
fileType: 'text/plain',
|
|
298
|
+
});
|
|
299
|
+
await fileModel.create({
|
|
300
|
+
name: 'test-file-2.txt',
|
|
301
|
+
url: 'https://example.com/test-file-2.txt',
|
|
302
|
+
size: 200,
|
|
303
|
+
fileType: 'text/plain',
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await fileModel.clear();
|
|
165
307
|
|
|
166
|
-
|
|
167
|
-
|
|
308
|
+
const userFiles = await serverDB.query.files.findMany({ where: eq(files.userId, userId) });
|
|
309
|
+
expect(userFiles).toHaveLength(0);
|
|
310
|
+
});
|
|
168
311
|
});
|
|
169
312
|
|
|
170
313
|
describe('Query', () => {
|
|
@@ -334,22 +477,24 @@ describe('FileModel', () => {
|
|
|
334
477
|
});
|
|
335
478
|
});
|
|
336
479
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
480
|
+
describe('findById', () => {
|
|
481
|
+
it('should find a file by id', async () => {
|
|
482
|
+
const { id } = await fileModel.create({
|
|
483
|
+
name: 'test-file.txt',
|
|
484
|
+
url: 'https://example.com/test-file.txt',
|
|
485
|
+
size: 100,
|
|
486
|
+
fileType: 'text/plain',
|
|
487
|
+
});
|
|
344
488
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
489
|
+
const file = await fileModel.findById(id);
|
|
490
|
+
expect(file).toMatchObject({
|
|
491
|
+
id,
|
|
492
|
+
name: 'test-file.txt',
|
|
493
|
+
url: 'https://example.com/test-file.txt',
|
|
494
|
+
size: 100,
|
|
495
|
+
fileType: 'text/plain',
|
|
496
|
+
userId,
|
|
497
|
+
});
|
|
353
498
|
});
|
|
354
499
|
});
|
|
355
500
|
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
import { eq } from 'drizzle-orm';
|
|
3
|
-
import { desc } from 'drizzle-orm/expressions';
|
|
3
|
+
import { and, desc } from 'drizzle-orm/expressions';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
6
|
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
NewKnowledgeBase,
|
|
10
|
+
files,
|
|
11
|
+
globalFiles,
|
|
12
|
+
knowledgeBaseFiles,
|
|
13
|
+
knowledgeBases,
|
|
14
|
+
users,
|
|
15
|
+
} from '../../schemas/lobechat';
|
|
9
16
|
import { KnowledgeBaseModel } from '../knowledgeBase';
|
|
10
17
|
|
|
11
18
|
let serverDB = await getTestDBInstance();
|
|
@@ -21,6 +28,7 @@ const knowledgeBaseModel = new KnowledgeBaseModel(userId);
|
|
|
21
28
|
|
|
22
29
|
beforeEach(async () => {
|
|
23
30
|
await serverDB.delete(users);
|
|
31
|
+
await serverDB.delete(globalFiles);
|
|
24
32
|
await serverDB.insert(users).values([{ id: userId }, { id: 'user2' }]);
|
|
25
33
|
});
|
|
26
34
|
|
|
@@ -129,4 +137,122 @@ describe('KnowledgeBaseModel', () => {
|
|
|
129
137
|
});
|
|
130
138
|
});
|
|
131
139
|
});
|
|
140
|
+
|
|
141
|
+
const fileList = [
|
|
142
|
+
{
|
|
143
|
+
id: 'file1',
|
|
144
|
+
name: 'document.pdf',
|
|
145
|
+
url: 'https://example.com/document.pdf',
|
|
146
|
+
fileHash: 'hash1',
|
|
147
|
+
size: 1000,
|
|
148
|
+
fileType: 'application/pdf',
|
|
149
|
+
userId,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'file2',
|
|
153
|
+
name: 'image.jpg',
|
|
154
|
+
url: 'https://example.com/image.jpg',
|
|
155
|
+
fileHash: 'hash2',
|
|
156
|
+
size: 500,
|
|
157
|
+
fileType: 'image/jpeg',
|
|
158
|
+
userId,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
describe('addFilesToKnowledgeBase', () => {
|
|
163
|
+
it('should add files to a knowledge base', async () => {
|
|
164
|
+
await serverDB.insert(globalFiles).values([
|
|
165
|
+
{
|
|
166
|
+
hashId: 'hash1',
|
|
167
|
+
url: 'https://example.com/document.pdf',
|
|
168
|
+
size: 1000,
|
|
169
|
+
fileType: 'application/pdf',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
hashId: 'hash2',
|
|
173
|
+
url: 'https://example.com/image.jpg',
|
|
174
|
+
size: 500,
|
|
175
|
+
fileType: 'image/jpeg',
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
await serverDB.insert(files).values(fileList);
|
|
180
|
+
|
|
181
|
+
const { id: knowledgeBaseId } = await knowledgeBaseModel.create({ name: 'Test Group' });
|
|
182
|
+
const fileIds = ['file1', 'file2'];
|
|
183
|
+
|
|
184
|
+
const result = await knowledgeBaseModel.addFilesToKnowledgeBase(knowledgeBaseId, fileIds);
|
|
185
|
+
|
|
186
|
+
expect(result).toHaveLength(2);
|
|
187
|
+
expect(result).toEqual(
|
|
188
|
+
expect.arrayContaining(
|
|
189
|
+
fileIds.map((fileId) => expect.objectContaining({ fileId, knowledgeBaseId })),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const addedFiles = await serverDB.query.knowledgeBaseFiles.findMany({
|
|
194
|
+
where: eq(knowledgeBaseFiles.knowledgeBaseId, knowledgeBaseId),
|
|
195
|
+
});
|
|
196
|
+
expect(addedFiles).toHaveLength(2);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('removeFilesFromKnowledgeBase', () => {
|
|
201
|
+
it('should remove files from a knowledge base', async () => {
|
|
202
|
+
await serverDB.insert(globalFiles).values([
|
|
203
|
+
{
|
|
204
|
+
hashId: 'hash1',
|
|
205
|
+
url: 'https://example.com/document.pdf',
|
|
206
|
+
size: 1000,
|
|
207
|
+
fileType: 'application/pdf',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
hashId: 'hash2',
|
|
211
|
+
url: 'https://example.com/image.jpg',
|
|
212
|
+
size: 500,
|
|
213
|
+
fileType: 'image/jpeg',
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
await serverDB.insert(files).values(fileList);
|
|
218
|
+
|
|
219
|
+
const { id: knowledgeBaseId } = await knowledgeBaseModel.create({ name: 'Test Group' });
|
|
220
|
+
const fileIds = ['file1', 'file2'];
|
|
221
|
+
await knowledgeBaseModel.addFilesToKnowledgeBase(knowledgeBaseId, fileIds);
|
|
222
|
+
|
|
223
|
+
const filesToRemove = ['file1'];
|
|
224
|
+
await knowledgeBaseModel.removeFilesFromKnowledgeBase(knowledgeBaseId, filesToRemove);
|
|
225
|
+
|
|
226
|
+
const remainingFiles = await serverDB.query.knowledgeBaseFiles.findMany({
|
|
227
|
+
where: and(eq(knowledgeBaseFiles.knowledgeBaseId, knowledgeBaseId)),
|
|
228
|
+
});
|
|
229
|
+
expect(remainingFiles).toHaveLength(1);
|
|
230
|
+
expect(remainingFiles[0].fileId).toBe('file2');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('static findById', () => {
|
|
235
|
+
it('should find a knowledge base by id without user restriction', async () => {
|
|
236
|
+
const { id } = await knowledgeBaseModel.create({ name: 'Test Group' });
|
|
237
|
+
|
|
238
|
+
const group = await KnowledgeBaseModel.findById(id);
|
|
239
|
+
expect(group).toMatchObject({
|
|
240
|
+
id,
|
|
241
|
+
name: 'Test Group',
|
|
242
|
+
userId,
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should find a knowledge base created by another user', async () => {
|
|
247
|
+
const anotherKnowledgeBaseModel = new KnowledgeBaseModel('user2');
|
|
248
|
+
const { id } = await anotherKnowledgeBaseModel.create({ name: 'Another User Group' });
|
|
249
|
+
|
|
250
|
+
const group = await KnowledgeBaseModel.findById(id);
|
|
251
|
+
expect(group).toMatchObject({
|
|
252
|
+
id,
|
|
253
|
+
name: 'Another User Group',
|
|
254
|
+
userId: 'user2',
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
132
258
|
});
|
|
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
4
4
|
import { INBOX_SESSION_ID } from '@/const/session';
|
|
5
5
|
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
|
6
6
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
|
7
|
-
import { UserPreference } from '@/types/user';
|
|
7
|
+
import { UserGuide, UserPreference } from '@/types/user';
|
|
8
8
|
import { UserSettings } from '@/types/user/settings';
|
|
9
9
|
|
|
10
10
|
import { userSettings, users } from '../../schemas/lobechat';
|
|
@@ -167,6 +167,24 @@ describe('UserModel', () => {
|
|
|
167
167
|
const { plaintext } = await gateKeeper.decrypt(updatedSettings!.keyVaults!);
|
|
168
168
|
expect(JSON.parse(plaintext)).toEqual(settings.keyVaults);
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
it('should update user settings with encrypted keyVaults', async () => {
|
|
172
|
+
const settings = {
|
|
173
|
+
general: { language: 'en-US' },
|
|
174
|
+
} as UserSettings;
|
|
175
|
+
await serverDB.insert(users).values({ id: userId });
|
|
176
|
+
await serverDB.insert(userSettings).values({ ...settings, keyVaults: '', id: userId });
|
|
177
|
+
|
|
178
|
+
const newSettings = {
|
|
179
|
+
general: { fontSize: 16, language: 'zh-CN', themeMode: 'dark' },
|
|
180
|
+
} as UserSettings;
|
|
181
|
+
await userModel.updateSetting(userId, newSettings);
|
|
182
|
+
|
|
183
|
+
const updatedSettings = await serverDB.query.userSettings.findFirst({
|
|
184
|
+
where: eq(users.id, userId),
|
|
185
|
+
});
|
|
186
|
+
expect(updatedSettings?.general).toEqual(newSettings.general);
|
|
187
|
+
});
|
|
170
188
|
});
|
|
171
189
|
|
|
172
190
|
describe('updatePreference', () => {
|
|
@@ -183,4 +201,21 @@ describe('UserModel', () => {
|
|
|
183
201
|
expect(updatedUser?.preference).toEqual({ ...preference, ...newPreference });
|
|
184
202
|
});
|
|
185
203
|
});
|
|
204
|
+
|
|
205
|
+
describe('updateGuide', () => {
|
|
206
|
+
it('should update user guide', async () => {
|
|
207
|
+
const preference = { guide: { topic: false } } as UserGuide;
|
|
208
|
+
await serverDB.insert(users).values({ id: userId, preference });
|
|
209
|
+
|
|
210
|
+
const newGuide: Partial<UserGuide> = {
|
|
211
|
+
topic: true,
|
|
212
|
+
moveSettingsToAvatar: true,
|
|
213
|
+
uploadFileInKnowledgeBase: true,
|
|
214
|
+
};
|
|
215
|
+
await userModel.updateGuide(userId, newGuide);
|
|
216
|
+
|
|
217
|
+
const updatedUser = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
|
|
218
|
+
expect(updatedUser?.preference).toEqual({ ...preference, guide: newGuide });
|
|
219
|
+
});
|
|
220
|
+
});
|
|
186
221
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { asc, cosineDistance, count, eq, inArray, sql } from 'drizzle-orm';
|
|
2
|
-
import { and, desc } from 'drizzle-orm/expressions';
|
|
2
|
+
import { and, desc, isNull } from 'drizzle-orm/expressions';
|
|
3
3
|
|
|
4
4
|
import { serverDB } from '@/database/server';
|
|
5
5
|
import { ChunkMetadata, FileChunk, SemanticSearchChunk } from '@/types/chunk';
|
|
@@ -43,6 +43,19 @@ export class ChunkModel {
|
|
|
43
43
|
return serverDB.delete(chunks).where(and(eq(chunks.id, id), eq(chunks.userId, this.userId)));
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
deleteOrphanChunks = async () => {
|
|
47
|
+
const orphanedChunks = await serverDB
|
|
48
|
+
.select({ chunkId: chunks.id })
|
|
49
|
+
.from(chunks)
|
|
50
|
+
.leftJoin(fileChunks, eq(chunks.id, fileChunks.chunkId))
|
|
51
|
+
.where(isNull(fileChunks.fileId));
|
|
52
|
+
|
|
53
|
+
const ids = orphanedChunks.map((chunk) => chunk.chunkId);
|
|
54
|
+
if (ids.length === 0) return;
|
|
55
|
+
|
|
56
|
+
await serverDB.delete(chunks).where(inArray(chunks.id, ids));
|
|
57
|
+
};
|
|
58
|
+
|
|
46
59
|
findById = async (id: string) => {
|
|
47
60
|
return serverDB.query.chunks.findFirst({
|
|
48
61
|
where: and(eq(chunks.id, id)),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { asc, count, eq, ilike, inArray, notExists } from 'drizzle-orm';
|
|
2
2
|
import { and, desc } from 'drizzle-orm/expressions';
|
|
3
3
|
|
|
4
|
+
import { serverDBEnv } from '@/config/db';
|
|
4
5
|
import { serverDB } from '@/database/server/core/db';
|
|
5
6
|
import { FilesTabs, QueryFileListParams, SortType } from '@/types/files';
|
|
6
7
|
|
|
@@ -77,7 +78,8 @@ export class FileModel {
|
|
|
77
78
|
const fileCount = result[0].count;
|
|
78
79
|
|
|
79
80
|
// delete the file from global file if it is not used by other files
|
|
80
|
-
if
|
|
81
|
+
// if `DISABLE_REMOVE_GLOBAL_FILE` is true, we will not remove the global file
|
|
82
|
+
if (fileCount === 0 && !serverDBEnv.DISABLE_REMOVE_GLOBAL_FILE) {
|
|
81
83
|
await trx.delete(globalFiles).where(eq(globalFiles.hashId, fileHash));
|
|
82
84
|
|
|
83
85
|
return file;
|
|
@@ -118,7 +120,7 @@ export class FileModel {
|
|
|
118
120
|
|
|
119
121
|
const needToDeleteList = fileHashCounts.filter((item) => item.count === 0);
|
|
120
122
|
|
|
121
|
-
if (needToDeleteList.length === 0) return;
|
|
123
|
+
if (needToDeleteList.length === 0 || serverDBEnv.DISABLE_REMOVE_GLOBAL_FILE) return;
|
|
122
124
|
|
|
123
125
|
// delete the file from global file if it is not used by other files
|
|
124
126
|
await trx.delete(globalFiles).where(
|
|
@@ -150,6 +150,8 @@ export const fileRouter = router({
|
|
|
150
150
|
removeFile: fileProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
|
|
151
151
|
const file = await ctx.fileModel.delete(input.id);
|
|
152
152
|
|
|
153
|
+
// delete the orphan chunks
|
|
154
|
+
await ctx.chunkModel.deleteOrphanChunks();
|
|
153
155
|
if (!file) return;
|
|
154
156
|
|
|
155
157
|
// delele the file from remove from S3 if it is not used by other files
|
|
@@ -162,6 +164,9 @@ export const fileRouter = router({
|
|
|
162
164
|
.mutation(async ({ input, ctx }) => {
|
|
163
165
|
const needToRemoveFileList = await ctx.fileModel.deleteMany(input.ids);
|
|
164
166
|
|
|
167
|
+
// delete the orphan chunks
|
|
168
|
+
await ctx.chunkModel.deleteOrphanChunks();
|
|
169
|
+
|
|
165
170
|
if (!needToRemoveFileList || needToRemoveFileList.length === 0) return;
|
|
166
171
|
|
|
167
172
|
// remove from S3
|