@lobehub/lobehub 2.0.5 → 2.0.6
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/.eslintrc.js +1 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v2.json +9 -0
- package/docs/self-hosting/advanced/auth/clerk-to-betterauth.mdx +2 -0
- package/docs/self-hosting/advanced/auth/clerk-to-betterauth.zh-CN.mdx +2 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +2 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +2 -0
- package/docs/self-hosting/platform/docker.mdx +6 -0
- package/docs/self-hosting/platform/docker.zh-CN.mdx +6 -0
- package/docs/self-hosting/platform/vercel.mdx +0 -49
- package/docs/self-hosting/platform/vercel.zh-CN.mdx +0 -47
- package/docs/self-hosting/start.mdx +0 -20
- package/docs/self-hosting/start.zh-CN.mdx +0 -18
- package/package.json +1 -1
- package/src/app/(backend)/api/workflows/memory-user-memory/pipelines/persona/update-writing/route.ts +19 -19
- package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/ToolAuthAlert.tsx +3 -2
- package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +3 -3
- package/src/app/[variants]/onboarding/components/KlavisServerList/hooks/useKlavisOAuth.ts +12 -5
- package/src/components/DebugNode.tsx +21 -0
- package/src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx +3 -3
- package/src/features/ChatInput/ActionBar/Tools/ToolItem.tsx +16 -13
- package/src/features/ChatInput/ActionBar/Tools/ToolsList.tsx +51 -40
- package/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx +7 -1
- package/src/features/ChatInput/ActionBar/components/ActionPopover.tsx +14 -11
- package/src/server/routers/lambda/klavis.ts +38 -10
- package/src/server/services/memory/userMemory/extract.ts +1 -1
- package/src/store/tool/slices/klavisStore/action.ts +20 -0
- package/docs/self-hosting/server-database.mdx +0 -157
- package/docs/self-hosting/server-database.zh-CN.mdx +0 -146
package/.eslintrc.js
CHANGED
|
@@ -20,6 +20,7 @@ config.rules['unicorn/no-array-for-each'] = 0;
|
|
|
20
20
|
config.rules['unicorn/prefer-number-properties'] = 0;
|
|
21
21
|
config.rules['unicorn/prefer-query-selector'] = 0;
|
|
22
22
|
config.rules['unicorn/no-array-callback-reference'] = 0;
|
|
23
|
+
config.rules['@typescript-eslint/no-use-before-define'] = 0;
|
|
23
24
|
// FIXME: Linting error in src/app/[variants]/(main)/chat/features/Migration/DBReader.ts, the fundamental solution should be upgrading typescript-eslint
|
|
24
25
|
config.rules['@typescript-eslint/no-useless-constructor'] = 0;
|
|
25
26
|
config.rules['@next/next/no-img-element'] = 0;
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 2.0.6](https://github.com/lobehub/lobe-chat/compare/v2.0.5...v2.0.6)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-27**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: The klavis in onboarding connect timeout fixed.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: The klavis in onboarding connect timeout fixed, closes [#11918](https://github.com/lobehub/lobe-chat/issues/11918) ([bc165be](https://github.com/lobehub/lobe-chat/commit/bc165be))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 2.0.5](https://github.com/lobehub/lobe-chat/compare/v2.0.4...v2.0.5)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2026-01-27**</sup>
|
package/changelog/v2.json
CHANGED
|
@@ -63,6 +63,8 @@ For small self-hosted deployments, the simplest approach is to let users reset t
|
|
|
63
63
|
|
|
64
64
|
Remove Clerk variables and add Better Auth variables:
|
|
65
65
|
|
|
66
|
+
<GenerateSecret envName="AUTH_SECRET" />
|
|
67
|
+
|
|
66
68
|
```bash
|
|
67
69
|
# Remove these
|
|
68
70
|
# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=xxx
|
|
@@ -116,6 +116,8 @@ For small self-hosted deployments, the simplest approach is to let users re-logi
|
|
|
116
116
|
|
|
117
117
|
Remove NextAuth variables and add Better Auth variables:
|
|
118
118
|
|
|
119
|
+
<GenerateSecret envName="AUTH_SECRET" />
|
|
120
|
+
|
|
119
121
|
```bash
|
|
120
122
|
# Remove these
|
|
121
123
|
# NEXT_AUTH_SECRET=xxx
|
|
@@ -58,6 +58,12 @@ Here is the process for deploying the LobeHub server database version on a Linux
|
|
|
58
58
|
|
|
59
59
|
### Create a file named `lobe-chat.env` to store environment variables:
|
|
60
60
|
|
|
61
|
+
Click the buttons below to generate required secrets:
|
|
62
|
+
|
|
63
|
+
<GenerateSecret envName="KEY_VAULTS_SECRET" />
|
|
64
|
+
|
|
65
|
+
<GenerateSecret envName="AUTH_SECRET" />
|
|
66
|
+
|
|
61
67
|
```shell
|
|
62
68
|
# Website domain
|
|
63
69
|
APP_URL=https://your-prod-domain.com
|
|
@@ -114,55 +114,6 @@ The server-side database needs to be paired with a user authentication service t
|
|
|
114
114
|
<Callout type={'info'}>
|
|
115
115
|
For advanced features like SSO providers, magic link login, and email verification, see [Authentication Service](/docs/self-hosting/advanced/auth).
|
|
116
116
|
</Callout>
|
|
117
|
-
|
|
118
|
-
### Add Public and Private Key Environment Variables in Vercel
|
|
119
|
-
|
|
120
|
-
In Vercel's deployment environment variables, add the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` environment variables. You can click on "API Keys" in the menu, then copy the corresponding values and paste them into Vercel's environment variables.
|
|
121
|
-
|
|
122
|
-
<Image alt={'Find the corresponding public and private key environment variables in Clerk'} src={'/blog/assets28616219/89883703-7a1a-4a11-b944-5d804544e57c.webp'} />
|
|
123
|
-
|
|
124
|
-
The environment variables required for this step are as follows:
|
|
125
|
-
|
|
126
|
-
```shell
|
|
127
|
-
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx
|
|
128
|
-
CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Add the above variables to Vercel:
|
|
132
|
-
|
|
133
|
-
<Image alt={'Add Clerk public and private key environment variables in Vercel'} src={'/blog/assets28616219/2bfa13df-6e20-4768-97c0-4dad06c85a2f.webp'} />
|
|
134
|
-
|
|
135
|
-
### Create and Configure Webhook in Clerk
|
|
136
|
-
|
|
137
|
-
Since we let Clerk fully handle user authentication and management, we need Clerk to notify our application and store data in the database when there are changes in the user's lifecycle (create, update, delete). We achieve this requirement through the Webhook provided by Clerk.
|
|
138
|
-
|
|
139
|
-
We need to add an endpoint in Clerk's Webhooks to inform Clerk to send notifications to this endpoint when a user's information changes.
|
|
140
|
-
|
|
141
|
-
<Image alt={'Add Webhooks endpoint in Clerk'} src={'/blog/assets28616219/f50f47fb-5e8e-4930-bf4e-8cf6f5b8afb9.webp'} />
|
|
142
|
-
|
|
143
|
-
Fill in the endpoint with the URL of your Vercel project, such as `https://your-project.vercel.app/api/webhooks/clerk`. Then, subscribe to events by checking the three user events (`user.created`, `user.deleted`, `user.updated`), and click create.
|
|
144
|
-
|
|
145
|
-
<Callout type={'warning'}>
|
|
146
|
-
The `https://` in the URL is essential to maintain the integrity of the URL.
|
|
147
|
-
</Callout>
|
|
148
|
-
|
|
149
|
-
<Image alt={'Configure URL and user events when adding Clerk Webhooks'} src={'/blog/assets28616219/0249ea56-ab17-4aa9-a56c-9ebd556c2645.webp'} />
|
|
150
|
-
|
|
151
|
-
### Add Webhook Secret to Vercel Environment Variables
|
|
152
|
-
|
|
153
|
-
After creation, you can find the secret of this Webhook in the bottom right corner:
|
|
154
|
-
|
|
155
|
-
<Image alt={'View Clerk Webhooks secret'} src={'/blog/assets28616219/fab4abb2-584b-49de-9340-813382951635.webp'} />
|
|
156
|
-
|
|
157
|
-
The environment variable corresponding to this secret is `CLERK_WEBHOOK_SECRET`:
|
|
158
|
-
|
|
159
|
-
```shell
|
|
160
|
-
CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Add it to Vercel's environment variables:
|
|
164
|
-
|
|
165
|
-
<Image alt={'Add Clerk Webhooks secret in Vercel'} src={'/blog/assets28616219/5fdc9479-007f-46ab-9d6e-a9603e949116.webp'} />
|
|
166
117
|
</Steps>
|
|
167
118
|
|
|
168
119
|
By completing these steps, you have successfully configured the authentication service. Next, we will configure the S3 storage service.
|
|
@@ -114,53 +114,6 @@ tags:
|
|
|
114
114
|
<Callout type={'info'}>
|
|
115
115
|
如需 SSO 登录、魔法链接登录、邮箱验证等高级功能,请参阅 [身份验证服务](/zh/docs/self-hosting/advanced/auth)。
|
|
116
116
|
</Callout>
|
|
117
|
-
|
|
118
|
-
### 在 Vercel 中添加公、私钥环境变量
|
|
119
|
-
|
|
120
|
-
在 Vercel 的部署环境变量中,添加 `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` 和 `CLERK_SECRET_KEY` 环境变量。你可以在菜单中点击「API Keys」,然后复制对应的值填入 Vercel 的环境变量中。
|
|
121
|
-
|
|
122
|
-
<Image alt={'在 Clerk 中找到对应的公私钥环境变量'} src={'/blog/assets28616219/89883703-7a1a-4a11-b944-5d804544e57c.webp'} />
|
|
123
|
-
|
|
124
|
-
此步骤所需的环境变量如下:
|
|
125
|
-
|
|
126
|
-
```shell
|
|
127
|
-
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx
|
|
128
|
-
CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
添加上述变量到 Vercel 中:
|
|
132
|
-
|
|
133
|
-
<Image alt={'在 Vercel 中添加 Clerk 公私钥环境变量'} src={'/blog/assets28616219/2bfa13df-6e20-4768-97c0-4dad06c85a2f.webp'} />
|
|
134
|
-
|
|
135
|
-
### 在 Clerk 中创建并配置 Webhook
|
|
136
|
-
|
|
137
|
-
由于我们让 Clerk 完全接管用户鉴权与管理,因此我们需要在 Clerk 用户生命周期变更时(创建、更新、删除)中通知我们的应用并存储落库。我们通过 Clerk 提供的 Webhook 来实现这一诉求。
|
|
138
|
-
|
|
139
|
-
我们需要在 Clerk 的 Webhooks 中添加一个端点(Endpoint),告诉 Clerk 当用户发生变更时,向这个端点发送通知。
|
|
140
|
-
|
|
141
|
-
<Image alt={'Clerk 添加 Webhooks 端点'} src={'/blog/assets28616219/f50f47fb-5e8e-4930-bf4e-8cf6f5b8afb9.webp'} />
|
|
142
|
-
|
|
143
|
-
在 endpoint 中填写你的 Vercel 项目的 URL,如 `https://your-project.vercel.app/api/webhooks/clerk`。然后在订阅事件(Subscribe to events)中,勾选 user 的三个事件(`user.created` 、`user.deleted`、`user.updated`),然后点击创建。
|
|
144
|
-
|
|
145
|
-
<Callout type={'warning'}>URL 的`https://`不可缺失,须保持 URL 的完整性</Callout>
|
|
146
|
-
|
|
147
|
-
<Image alt={'添加 Clerk Webhooks 时,配置 URL 和用户事件'} src={'/blog/assets28616219/0249ea56-ab17-4aa9-a56c-9ebd556c2645.webp'} />
|
|
148
|
-
|
|
149
|
-
### 将 Webhook 秘钥添加到 Vercel 环境变量
|
|
150
|
-
|
|
151
|
-
创建完毕后,可以在右下角找到该 Webhook 的秘钥:
|
|
152
|
-
|
|
153
|
-
<Image alt={'查看 Clerk Webhooks 秘钥'} src={'/blog/assets28616219/fab4abb2-584b-49de-9340-813382951635.webp'} />
|
|
154
|
-
|
|
155
|
-
这个秘钥所对应的环境变量名为 `CLERK_WEBHOOK_SECRET`:
|
|
156
|
-
|
|
157
|
-
```shell
|
|
158
|
-
CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
将其添加到 Vercel 的环境变量中:
|
|
162
|
-
|
|
163
|
-
<Image alt={'在 Vercel 中 添加 Clerk Webhooks 秘钥'} src={'/blog/assets28616219/5fdc9479-007f-46ab-9d6e-a9603e949116.webp'} />
|
|
164
117
|
</Steps>
|
|
165
118
|
|
|
166
119
|
这样,你已经成功配置了身份验证服务。接下来我们将配置 S3 存储服务。
|
|
@@ -16,24 +16,4 @@ tags:
|
|
|
16
16
|
|
|
17
17
|
LobeHub supports various deployment platforms, including Vercel, Docker, and Docker Compose. You can choose a deployment platform that suits you to build your own Lobe Chat.
|
|
18
18
|
|
|
19
|
-
## Quick Deployment
|
|
20
|
-
|
|
21
|
-
For users who are new to LobeHub, we recommend using the client-side database mode for quick deployment. The advantage of this mode is that deployment can be quickly completed with just one command/button, making it easy for you to quickly get started and experience LobeHub.
|
|
22
|
-
|
|
23
|
-
You can follow the guide below for quick deployment of LobeHub:
|
|
24
|
-
|
|
25
19
|
<PlatformCards urlPrefix={'platform'} />
|
|
26
|
-
|
|
27
|
-
<Callout>
|
|
28
|
-
In the client-side database mode, data is stored locally on the user's device, without
|
|
29
|
-
cross-device synchronization, and does not support advanced features such as file uploads and
|
|
30
|
-
knowledge base.
|
|
31
|
-
</Callout>
|
|
32
|
-
|
|
33
|
-
## Advanced Mode: Server-Side Database
|
|
34
|
-
|
|
35
|
-
For users who are already familiar with LobeHub or need cross-device synchronization, you can deploy a version with a server-side database to access a more complete and powerful LobeHub.
|
|
36
|
-
|
|
37
|
-
<Cards>
|
|
38
|
-
<Card href={'/docs/self-hosting/server-database'} title={'Server-Side Database Deployment Guide'} />
|
|
39
|
-
</Cards>
|
|
@@ -20,22 +20,4 @@ tags:
|
|
|
20
20
|
|
|
21
21
|
LobeHub 支持多种部署平台,包括 Vercel、Docker、 Docker Compose 、阿里云计算巢 和腾讯轻量云 等,你可以选择适合自己的部署平台进行部署,构建属于自己的 Lobe Chat。
|
|
22
22
|
|
|
23
|
-
## 快速部署
|
|
24
|
-
|
|
25
|
-
对于第一次了解 LobeHub 的用户,我们推荐使用客户端数据库的模式快速部署,该模式的优势是一行指令 / 一个按钮即可快捷完成部署,便于你快速上手与体验 LobeHub。
|
|
26
|
-
|
|
27
|
-
你可以通过以下指南快速部署 LobeHub:
|
|
28
|
-
|
|
29
23
|
<PlatformCards urlPrefix={'platform'} />
|
|
30
|
-
|
|
31
|
-
<Callout>
|
|
32
|
-
客户端数据库模式下数据均保留在用户本地,不会跨多端同步,也不支持文件上传、知识库等进阶功能。
|
|
33
|
-
</Callout>
|
|
34
|
-
|
|
35
|
-
## 进阶模式:服务端数据库
|
|
36
|
-
|
|
37
|
-
针对已经了解 LobeHub 的用户,或需要多端同步的用户,可以自行部署带有服务端数据库的版本,进而获得更完整、功能更强大的 LobeHub。
|
|
38
|
-
|
|
39
|
-
<Cards rows={1}>
|
|
40
|
-
<Card href={'/zh/docs/self-hosting/server-database'} title={'服务端数据库部署指南'} />
|
|
41
|
-
</Cards>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent 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",
|
package/src/app/(backend)/api/workflows/memory-user-memory/pipelines/persona/update-writing/route.ts
CHANGED
|
@@ -8,39 +8,39 @@ import {
|
|
|
8
8
|
} from '@/server/services/memory/userMemory/persona/service';
|
|
9
9
|
|
|
10
10
|
const workflowPayloadSchema = z.object({
|
|
11
|
-
userId: z.string().optional(),
|
|
12
11
|
userIds: z.array(z.string()).optional(),
|
|
13
12
|
});
|
|
14
13
|
|
|
15
14
|
export const { POST } = serve(async (context) => {
|
|
16
|
-
const payload =
|
|
15
|
+
const payload = await context.run('memory:pipelines:persona:update-writing:parse-payload', () =>
|
|
16
|
+
workflowPayloadSchema.parse(context.requestPayload || {}),
|
|
17
|
+
);
|
|
17
18
|
const db = await getServerDB();
|
|
18
19
|
|
|
19
|
-
const userIds = Array.from(
|
|
20
|
-
new Set([...(payload.userIds || []), ...(payload.userId ? [payload.userId] : [])]),
|
|
21
|
-
).filter(Boolean);
|
|
22
|
-
|
|
20
|
+
const userIds = Array.from(new Set(payload.userIds || [])).filter(Boolean);
|
|
23
21
|
if (userIds.length === 0) {
|
|
24
|
-
|
|
22
|
+
throw new Error('No user IDs provided for persona update.');
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
const service = new UserPersonaService(db);
|
|
28
|
-
const results = [];
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
await Promise.all(
|
|
28
|
+
userIds.map(async (userId) =>
|
|
29
|
+
context.run(`memory:pipelines:persona:update-writing:users:${userId}`, async () => {
|
|
30
|
+
const context = await buildUserPersonaJobInput(db, userId);
|
|
31
|
+
const result = await service.composeWriting({ ...context, userId });
|
|
32
|
+
return {
|
|
33
|
+
diffId: result.diff?.id,
|
|
34
|
+
documentId: result.document.id,
|
|
35
|
+
userId,
|
|
36
|
+
version: result.document.version,
|
|
37
|
+
};
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
40
41
|
|
|
41
42
|
return {
|
|
42
43
|
message: 'User persona processed via workflow.',
|
|
43
44
|
processedUsers: userIds.length,
|
|
44
|
-
results,
|
|
45
45
|
};
|
|
46
46
|
});
|
package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/ToolAuthAlert.tsx
CHANGED
|
@@ -104,7 +104,7 @@ const KlavisToolAuthItem = memo<KlavisToolAuthItemProps>(({ tool, onAuthComplete
|
|
|
104
104
|
try {
|
|
105
105
|
await refreshKlavisServerTools(identifier);
|
|
106
106
|
} catch (error) {
|
|
107
|
-
console.
|
|
107
|
+
console.debug('[Klavis] Polling check (expected during auth):', error);
|
|
108
108
|
}
|
|
109
109
|
}, POLL_INTERVAL_MS);
|
|
110
110
|
|
|
@@ -129,7 +129,8 @@ const KlavisToolAuthItem = memo<KlavisToolAuthItemProps>(({ tool, onAuthComplete
|
|
|
129
129
|
windowCheckIntervalRef.current = null;
|
|
130
130
|
}
|
|
131
131
|
oauthWindowRef.current = null;
|
|
132
|
-
|
|
132
|
+
// Start polling after window closes
|
|
133
|
+
startFallbackPolling(identifier);
|
|
133
134
|
}
|
|
134
135
|
} catch {
|
|
135
136
|
if (windowCheckIntervalRef.current) {
|
|
@@ -120,7 +120,7 @@ const KlavisSkillItem = memo<KlavisSkillItemProps>(({ serverType, server }) => {
|
|
|
120
120
|
try {
|
|
121
121
|
await refreshKlavisServerTools(serverName);
|
|
122
122
|
} catch (error) {
|
|
123
|
-
console.
|
|
123
|
+
console.debug('[Klavis] Polling check (expected during auth):', error);
|
|
124
124
|
}
|
|
125
125
|
}, POLL_INTERVAL_MS);
|
|
126
126
|
|
|
@@ -145,8 +145,8 @@ const KlavisSkillItem = memo<KlavisSkillItemProps>(({ serverType, server }) => {
|
|
|
145
145
|
windowCheckIntervalRef.current = null;
|
|
146
146
|
}
|
|
147
147
|
oauthWindowRef.current = null;
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
// Start polling after window closes
|
|
149
|
+
startFallbackPolling(serverName);
|
|
150
150
|
}
|
|
151
151
|
} catch {
|
|
152
152
|
console.log('[Klavis] COOP blocked window.closed access, falling back to polling');
|
|
@@ -5,6 +5,7 @@ import { KlavisServerStatus } from '@/store/tool/slices/klavisStore';
|
|
|
5
5
|
|
|
6
6
|
const POLL_INTERVAL_MS = 1000;
|
|
7
7
|
const POLL_TIMEOUT_MS = 15_000;
|
|
8
|
+
const WINDOW_CLOSED_POLL_TIMEOUT_MS = 4000; // Shorter timeout when window is closed
|
|
8
9
|
|
|
9
10
|
interface UseKlavisOAuthProps {
|
|
10
11
|
serverStatus?: KlavisServerStatus;
|
|
@@ -50,14 +51,14 @@ export const useKlavisOAuth = ({ serverStatus }: UseKlavisOAuthProps) => {
|
|
|
50
51
|
}, [serverStatus, isWaitingAuth, cleanup]);
|
|
51
52
|
|
|
52
53
|
const startFallbackPolling = useCallback(
|
|
53
|
-
(serverName: string) => {
|
|
54
|
+
(serverName: string, timeoutMs: number = POLL_TIMEOUT_MS) => {
|
|
54
55
|
if (pollIntervalRef.current) return;
|
|
55
56
|
|
|
56
57
|
pollIntervalRef.current = setInterval(async () => {
|
|
57
58
|
try {
|
|
58
59
|
await refreshKlavisServerTools(serverName);
|
|
59
60
|
} catch (error) {
|
|
60
|
-
console.
|
|
61
|
+
console.debug('[Klavis] Polling check (expected during auth):', error);
|
|
61
62
|
}
|
|
62
63
|
}, POLL_INTERVAL_MS);
|
|
63
64
|
|
|
@@ -67,7 +68,7 @@ export const useKlavisOAuth = ({ serverStatus }: UseKlavisOAuthProps) => {
|
|
|
67
68
|
pollIntervalRef.current = null;
|
|
68
69
|
}
|
|
69
70
|
setIsWaitingAuth(false);
|
|
70
|
-
},
|
|
71
|
+
}, timeoutMs);
|
|
71
72
|
},
|
|
72
73
|
[refreshKlavisServerTools],
|
|
73
74
|
);
|
|
@@ -77,23 +78,29 @@ export const useKlavisOAuth = ({ serverStatus }: UseKlavisOAuthProps) => {
|
|
|
77
78
|
windowCheckIntervalRef.current = setInterval(() => {
|
|
78
79
|
try {
|
|
79
80
|
if (oauthWindow.closed) {
|
|
81
|
+
// Stop monitoring window
|
|
80
82
|
if (windowCheckIntervalRef.current) {
|
|
81
83
|
clearInterval(windowCheckIntervalRef.current);
|
|
82
84
|
windowCheckIntervalRef.current = null;
|
|
83
85
|
}
|
|
84
86
|
oauthWindowRef.current = null;
|
|
85
|
-
|
|
87
|
+
|
|
88
|
+
// Start polling to check auth status after window closes
|
|
89
|
+
// Use shorter timeout since user has closed the window
|
|
90
|
+
// Keep loading state until we confirm success or timeout
|
|
91
|
+
startFallbackPolling(serverName, WINDOW_CLOSED_POLL_TIMEOUT_MS);
|
|
86
92
|
}
|
|
87
93
|
} catch {
|
|
88
94
|
if (windowCheckIntervalRef.current) {
|
|
89
95
|
clearInterval(windowCheckIntervalRef.current);
|
|
90
96
|
windowCheckIntervalRef.current = null;
|
|
91
97
|
}
|
|
98
|
+
// Use default timeout for fallback polling
|
|
92
99
|
startFallbackPolling(serverName);
|
|
93
100
|
}
|
|
94
101
|
}, 500);
|
|
95
102
|
},
|
|
96
|
-
[
|
|
103
|
+
[startFallbackPolling],
|
|
97
104
|
);
|
|
98
105
|
|
|
99
106
|
const openOAuthWindow = useCallback(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
interface DebugNodeProps {
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
trace: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DebugNode = ({ children, trace }: DebugNodeProps) => {
|
|
11
|
+
if (process.env.NODE_ENV !== 'development') return null;
|
|
12
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.log(`[DebugNode] Suspense fallback active: ${trace}`);
|
|
16
|
+
}, [trace]);
|
|
17
|
+
|
|
18
|
+
return children ?? null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default DebugNode;
|
|
@@ -88,7 +88,7 @@ const KlavisServerItem = memo<KlavisServerItemProps>(
|
|
|
88
88
|
try {
|
|
89
89
|
await refreshKlavisServerTools(serverName);
|
|
90
90
|
} catch (error) {
|
|
91
|
-
console.
|
|
91
|
+
console.debug('[Klavis] Polling check (expected during auth):', error);
|
|
92
92
|
}
|
|
93
93
|
}, POLL_INTERVAL_MS);
|
|
94
94
|
|
|
@@ -121,8 +121,8 @@ const KlavisServerItem = memo<KlavisServerItemProps>(
|
|
|
121
121
|
}
|
|
122
122
|
oauthWindowRef.current = null;
|
|
123
123
|
|
|
124
|
-
//
|
|
125
|
-
|
|
124
|
+
// 窗口关闭后开始轮询检查认证状态
|
|
125
|
+
startFallbackPolling(serverName);
|
|
126
126
|
}
|
|
127
127
|
} catch {
|
|
128
128
|
// COOP 阻止了访问,降级到轮询方案
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flexbox } from '@lobehub/ui';
|
|
2
|
-
import { memo } from 'react';
|
|
2
|
+
import { Suspense, memo } from 'react';
|
|
3
3
|
|
|
4
|
+
import DebugNode from '@/components/DebugNode';
|
|
4
5
|
import PluginTag from '@/components/Plugins/PluginTag';
|
|
5
6
|
import { useToolStore } from '@/store/tool';
|
|
6
7
|
import { customPluginSelectors } from '@/store/tool/selectors';
|
|
@@ -11,18 +12,20 @@ const ToolItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
|
|
|
11
12
|
const isCustom = useToolStore((s) => customPluginSelectors.isCustomPlugin(id)(s));
|
|
12
13
|
|
|
13
14
|
return (
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
<Suspense fallback={<DebugNode trace="ActionBar/Tools/ToolItem" />}>
|
|
16
|
+
<CheckboxItem
|
|
17
|
+
checked={checked}
|
|
18
|
+
hasPadding={false}
|
|
19
|
+
id={id}
|
|
20
|
+
label={
|
|
21
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
|
22
|
+
{label || id}
|
|
23
|
+
{isCustom && <PluginTag showText={false} type={'customPlugin'} />}
|
|
24
|
+
</Flexbox>
|
|
25
|
+
}
|
|
26
|
+
onUpdate={onUpdate}
|
|
27
|
+
/>
|
|
28
|
+
</Suspense>
|
|
26
29
|
);
|
|
27
30
|
});
|
|
28
31
|
|
|
@@ -59,51 +59,62 @@ interface ToolsListProps {
|
|
|
59
59
|
items: ItemType[];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return <Divider key={`divider-${index}`} style={{ margin: '4px 0' }} />;
|
|
66
|
-
}
|
|
62
|
+
const DividerItem = memo<{ index: number }>(({ index }) => (
|
|
63
|
+
<Divider key={`divider-${index}`} style={{ margin: '4px 0' }} />
|
|
64
|
+
));
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
const RegularItem = memo<{ index: number; item: ToolItemData }>(({ item, index }) => {
|
|
67
|
+
const iconNode = item.icon ? (
|
|
68
|
+
isValidElement(item.icon) ? (
|
|
69
|
+
item.icon
|
|
70
|
+
) : (
|
|
71
|
+
<Icon icon={item.icon as any} size={20} />
|
|
72
|
+
)
|
|
73
|
+
) : null;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
className={toolsListStyles.item}
|
|
78
|
+
key={item.key || `item-${index}`}
|
|
79
|
+
onClick={item.onClick}
|
|
80
|
+
role="button"
|
|
81
|
+
tabIndex={0}
|
|
82
|
+
>
|
|
83
|
+
{iconNode && <div className={toolsListStyles.itemIcon}>{iconNode}</div>}
|
|
84
|
+
<div className={toolsListStyles.itemContent}>{item.label}</div>
|
|
85
|
+
{item.extra}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
});
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<div
|
|
91
|
-
className={toolsListStyles.item}
|
|
92
|
-
key={item.key || `item-${index}`}
|
|
93
|
-
onClick={item.onClick}
|
|
94
|
-
role="button"
|
|
95
|
-
tabIndex={0}
|
|
96
|
-
>
|
|
97
|
-
{iconNode && <div className={toolsListStyles.itemIcon}>{iconNode}</div>}
|
|
98
|
-
<div className={toolsListStyles.itemContent}>{item.label}</div>
|
|
99
|
-
{item.extra}
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
};
|
|
90
|
+
const GroupItem = memo<{ index: number; item: ToolItemData }>(({ item, index }) => (
|
|
91
|
+
<Fragment key={item.key || `group-${index}`}>
|
|
92
|
+
<Text className={toolsListStyles.groupLabel} fontSize={12} type="secondary">
|
|
93
|
+
{item.label}
|
|
94
|
+
</Text>
|
|
95
|
+
{item.children?.map((child, childIndex) => (
|
|
96
|
+
<ToolListItem index={childIndex} item={child} key={child.key || `item-${childIndex}`} />
|
|
97
|
+
))}
|
|
98
|
+
</Fragment>
|
|
99
|
+
));
|
|
103
100
|
|
|
101
|
+
const ToolListItem = memo<{ index: number; item: ToolItemData | null }>(({ item, index }) => {
|
|
102
|
+
if (!item) return null;
|
|
103
|
+
if (item.type === 'divider') return <DividerItem index={index} />;
|
|
104
|
+
if (item.type === 'group') return <GroupItem index={index} item={item} />;
|
|
105
|
+
return <RegularItem index={index} item={item} />;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const ToolsList = memo<ToolsListProps>(({ items }) => {
|
|
104
109
|
return (
|
|
105
110
|
<Flexbox gap={0} padding={4}>
|
|
106
|
-
{items.map((item, index) =>
|
|
111
|
+
{items.map((item, index) => (
|
|
112
|
+
<ToolListItem
|
|
113
|
+
index={index}
|
|
114
|
+
item={item as ToolItemData | null}
|
|
115
|
+
key={item?.key || `item-${index}`}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
107
118
|
</Flexbox>
|
|
108
119
|
);
|
|
109
120
|
});
|
|
@@ -17,6 +17,7 @@ import { createStaticStyles, cx } from 'antd-style';
|
|
|
17
17
|
import {
|
|
18
18
|
type CSSProperties,
|
|
19
19
|
type ReactNode,
|
|
20
|
+
Suspense,
|
|
20
21
|
isValidElement,
|
|
21
22
|
memo,
|
|
22
23
|
useCallback,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
useState,
|
|
27
28
|
} from 'react';
|
|
28
29
|
|
|
30
|
+
import DebugNode from '@/components/DebugNode';
|
|
29
31
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
|
30
32
|
|
|
31
33
|
const styles = createStaticStyles(({ css }) => ({
|
|
@@ -271,7 +273,11 @@ const ActionDropdown = memo<ActionDropdownProps>(
|
|
|
271
273
|
hoverTrigger={Boolean(resolvedTriggerProps?.openOnHover)}
|
|
272
274
|
placement={isMobile ? 'top' : placement}
|
|
273
275
|
>
|
|
274
|
-
<DropdownMenuPopup {...resolvedPopupProps}>
|
|
276
|
+
<DropdownMenuPopup {...resolvedPopupProps}>
|
|
277
|
+
<Suspense fallback={<DebugNode trace="ActionDropdown > popup" />}>
|
|
278
|
+
{menuContent}
|
|
279
|
+
</Suspense>
|
|
280
|
+
</DropdownMenuPopup>
|
|
275
281
|
</DropdownMenuPositioner>
|
|
276
282
|
</DropdownMenuPortal>
|
|
277
283
|
</DropdownMenuRoot>
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { Flexbox, Popover, type PopoverProps } from '@lobehub/ui';
|
|
4
4
|
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
|
5
|
-
import { type ReactNode, memo } from 'react';
|
|
5
|
+
import { type ReactNode, Suspense, memo } from 'react';
|
|
6
6
|
|
|
7
|
+
import DebugNode from '@/components/DebugNode';
|
|
7
8
|
import UpdateLoading from '@/components/Loading/UpdateLoading';
|
|
8
9
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
|
9
10
|
|
|
@@ -65,16 +66,18 @@ const ActionPopover = memo<ActionPopoverProps>(
|
|
|
65
66
|
|
|
66
67
|
// Compose content with optional title
|
|
67
68
|
const popoverContent = (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
<Suspense fallback={<DebugNode trace="ActionPopover > content" />}>
|
|
70
|
+
<>
|
|
71
|
+
{title && (
|
|
72
|
+
<Flexbox gap={8} horizontal justify={'space-between'} style={{ marginBottom: 16 }}>
|
|
73
|
+
{title}
|
|
74
|
+
{extra}
|
|
75
|
+
{loading && <UpdateLoading style={{ color: cssVar.colorTextSecondary }} />}
|
|
76
|
+
</Flexbox>
|
|
77
|
+
)}
|
|
78
|
+
{content}
|
|
79
|
+
</>
|
|
80
|
+
</Suspense>
|
|
78
81
|
);
|
|
79
82
|
|
|
80
83
|
return (
|
|
@@ -124,6 +124,7 @@ export const klavisRouter = router({
|
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Get server instance status from Klavis API
|
|
127
|
+
* Returns error object instead of throwing on auth errors (useful for polling)
|
|
127
128
|
*/
|
|
128
129
|
getServerInstance: klavisProcedure
|
|
129
130
|
.input(
|
|
@@ -132,16 +133,43 @@ export const klavisRouter = router({
|
|
|
132
133
|
}),
|
|
133
134
|
)
|
|
134
135
|
.query(async ({ input, ctx }) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
136
|
+
try {
|
|
137
|
+
const response = await ctx.klavisClient.mcpServer.getServerInstance(input.instanceId);
|
|
138
|
+
return {
|
|
139
|
+
authNeeded: response.authNeeded,
|
|
140
|
+
error: undefined,
|
|
141
|
+
externalUserId: response.externalUserId,
|
|
142
|
+
instanceId: response.instanceId,
|
|
143
|
+
isAuthenticated: response.isAuthenticated,
|
|
144
|
+
oauthUrl: response.oauthUrl,
|
|
145
|
+
platform: response.platform,
|
|
146
|
+
serverName: response.serverName,
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// Check if this is an authentication error
|
|
150
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
151
|
+
const isAuthError =
|
|
152
|
+
errorMessage.includes('Invalid API key or instance ID') ||
|
|
153
|
+
errorMessage.includes('Status code: 401');
|
|
154
|
+
|
|
155
|
+
// For auth errors, return error object instead of throwing
|
|
156
|
+
// This prevents 500 errors in logs during polling
|
|
157
|
+
if (isAuthError) {
|
|
158
|
+
return {
|
|
159
|
+
authNeeded: true,
|
|
160
|
+
error: 'AUTH_ERROR',
|
|
161
|
+
externalUserId: undefined,
|
|
162
|
+
instanceId: input.instanceId,
|
|
163
|
+
isAuthenticated: false,
|
|
164
|
+
oauthUrl: undefined,
|
|
165
|
+
platform: undefined,
|
|
166
|
+
serverName: undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// For other errors, still throw
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
145
173
|
}),
|
|
146
174
|
|
|
147
175
|
getUserIntergrations: klavisProcedure
|
|
@@ -2266,7 +2266,7 @@ export class MemoryExtractionWorkflowService {
|
|
|
2266
2266
|
|
|
2267
2267
|
const url = getWorkflowUrl(WORKFLOW_PATHS.personaUpdate, baseUrl);
|
|
2268
2268
|
return this.getClient().trigger({
|
|
2269
|
-
body: { userId },
|
|
2269
|
+
body: { userIds: [userId] },
|
|
2270
2270
|
flowControl: {
|
|
2271
2271
|
key: `memory-user-memory.pipelines.persona.update-write.${userId}`,
|
|
2272
2272
|
parallelism: 1,
|
|
@@ -210,10 +210,30 @@ export const createKlavisStoreSlice: StateCreator<
|
|
|
210
210
|
instanceId: server.instanceId,
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
// If server returned an auth error (during polling), silently return
|
|
214
|
+
// This happens when user is still in the process of authorizing
|
|
215
|
+
if (instanceStatus.error === 'AUTH_ERROR') {
|
|
216
|
+
set(
|
|
217
|
+
produce((draft: KlavisStoreState) => {
|
|
218
|
+
draft.loadingServerIds.delete(identifier);
|
|
219
|
+
}),
|
|
220
|
+
false,
|
|
221
|
+
n('refreshKlavisServerTools/pendingAuth'),
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
213
226
|
// If authentication failed, remove server and reset status
|
|
214
227
|
if (!instanceStatus.isAuthenticated) {
|
|
215
228
|
if (!instanceStatus.authNeeded) {
|
|
216
229
|
// If no authentication needed, all is well
|
|
230
|
+
set(
|
|
231
|
+
produce((draft: KlavisStoreState) => {
|
|
232
|
+
draft.loadingServerIds.delete(identifier);
|
|
233
|
+
}),
|
|
234
|
+
false,
|
|
235
|
+
n('refreshKlavisServerTools/noAuthNeeded'),
|
|
236
|
+
);
|
|
217
237
|
return;
|
|
218
238
|
}
|
|
219
239
|
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Deploying Server-Side Database for LobeHub
|
|
3
|
-
description: Learn how to deploy LobeHub's server-side database using Postgres.
|
|
4
|
-
tags:
|
|
5
|
-
- LobeHub
|
|
6
|
-
- Server-Side Database
|
|
7
|
-
- Postgres
|
|
8
|
-
- Deployment Guide
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Deploying Server-Side Database
|
|
12
|
-
|
|
13
|
-
LobeHub defaults to using a client-side database (IndexedDB) but also supports deploying a server-side database. LobeHub uses Postgres as the backend storage database.
|
|
14
|
-
|
|
15
|
-
<Callout>
|
|
16
|
-
PostgreSQL is a powerful open-source relational database management system with high scalability
|
|
17
|
-
and standard SQL support. It provides rich data types, concurrency control, data integrity,
|
|
18
|
-
security, and programmability, making it suitable for complex applications and large-scale data
|
|
19
|
-
management.
|
|
20
|
-
</Callout>
|
|
21
|
-
|
|
22
|
-
This guide will introduce the process and principles of deploying the server-side database version of LobeHub on any platform from a framework perspective, so you can understand both the what and the why, and then deploy according to your specific needs.
|
|
23
|
-
|
|
24
|
-
If you are already familiar with the complete principles, you can quickly get started by checking the deployment guides for each platform:
|
|
25
|
-
|
|
26
|
-
<PlatformCards urlPrefix={'platform'} />
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
For the server-side database version of LobeHub, a normal deployment process typically involves configuring three modules:
|
|
31
|
-
|
|
32
|
-
1. Database configuration;
|
|
33
|
-
2. Authentication service configuration;
|
|
34
|
-
3. S3 storage service configuration.
|
|
35
|
-
|
|
36
|
-
## Configure the Database
|
|
37
|
-
|
|
38
|
-
Before deployment, make sure you have a Postgres database instance ready. You can choose from the following instances:
|
|
39
|
-
|
|
40
|
-
- `A.` Use Serverless Postgres instances like Vercel/Neon;
|
|
41
|
-
- `B.` Use self-deployed Postgres instances like Docker/Railway/Zeabur, collectively referred to as Node Postgres instances;
|
|
42
|
-
|
|
43
|
-
<Callout>
|
|
44
|
-
There is a slight difference in the way they are configured in terms of environment variables.
|
|
45
|
-
</Callout>
|
|
46
|
-
|
|
47
|
-
Since we support file-based conversations/knowledge base conversations, we need to install the `pgvector` plugin for Postgres. This plugin provides vector search capabilities and is a key component for LobeHub to implement RAG.
|
|
48
|
-
|
|
49
|
-
<Steps>
|
|
50
|
-
### `NEXT_PUBLIC_SERVICE_MODE`
|
|
51
|
-
|
|
52
|
-
LobeHub supports both client-side and server-side databases, so we provide an environment variable for switching modes, which is `NEXT_PUBLIC_SERVICE_MODE`, with a default value of `client`.
|
|
53
|
-
|
|
54
|
-
For server-side database deployment scenarios, you need to set `NEXT_PUBLIC_SERVICE_MODE` to `server`.
|
|
55
|
-
|
|
56
|
-
<Callout type={'info'}>
|
|
57
|
-
In the official `lobehub` Docker image, this environment variable is already set to
|
|
58
|
-
`server` by default. Therefore, if you deploy using the Docker image, you do not need to configure
|
|
59
|
-
this environment variable again.
|
|
60
|
-
</Callout>
|
|
61
|
-
|
|
62
|
-
<Callout type={'tip'}>
|
|
63
|
-
Since environment variables starting with `NEXT_PUBLIC` take effect in the front-end code, they cannot be modified through container runtime injection. (Refer to the `next.js` documentation [Configuring: Environment Variables | Next.js (nextjs.org)](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables)). This is why we chose to create a separate DB version image.
|
|
64
|
-
|
|
65
|
-
If you need to modify variables with the `NEXT_PUBLIC` prefix in a Docker deployment, you must build the image yourself and inject your own `NEXT_PUBLIC` prefixed environment variables during the build.
|
|
66
|
-
</Callout>
|
|
67
|
-
|
|
68
|
-
### `DATABASE_URL`
|
|
69
|
-
|
|
70
|
-
The core of configuring the database is to add the `DATABASE_URL` environment variable and fill in the Postgres database connection URL you have prepared. The typical format of the database connection URL is `postgres://username:password@host:port/database`.
|
|
71
|
-
|
|
72
|
-
<Callout type={'info'}>
|
|
73
|
-
If you want to enable SSL when connecting to the database, please refer to the
|
|
74
|
-
[documentation](https://stackoverflow.com/questions/14021998/using-psql-to-connect-to-postgresql-in-ssl-mode)
|
|
75
|
-
for setup instructions.
|
|
76
|
-
</Callout>
|
|
77
|
-
|
|
78
|
-
### `DATABASE_DRIVER`
|
|
79
|
-
|
|
80
|
-
The `DATABASE_DRIVER` environment variable is used to distinguish between the two types of Postgres database instances, with values of `node` or `neon`.
|
|
81
|
-
|
|
82
|
-
To streamline deployment, we have set default values based on the characteristics of different platforms:
|
|
83
|
-
|
|
84
|
-
- On the Vercel platform, `DATABASE_DRIVER` defaults to `neon`;
|
|
85
|
-
- In our provided Docker image `lobehub`, `DATABASE_DRIVER` defaults to `node`.
|
|
86
|
-
|
|
87
|
-
Therefore, if you follow the standard deployment methods below, you do not need to manually configure the `DATABASE_DRIVER` environment variable:
|
|
88
|
-
|
|
89
|
-
- Vercel + Serverless Postgres
|
|
90
|
-
- Docker image + Node Postgres
|
|
91
|
-
|
|
92
|
-
### `KEY_VAULTS_SECRET`
|
|
93
|
-
|
|
94
|
-
Considering that users will store sensitive information such as their API Key and baseURL in the database, we need a key to encrypt this information to prevent leakage in case of a database breach. Hence, the `KEY_VAULTS_SECRET` environment variable is used to encrypt sensitive information like user-stored apikeys.
|
|
95
|
-
|
|
96
|
-
<Callout type={'info'}>
|
|
97
|
-
You can generate a random 32-character string as the value of `KEY_VAULTS_SECRET` using `openssl
|
|
98
|
-
rand -base64 32`.
|
|
99
|
-
</Callout>
|
|
100
|
-
</Steps>
|
|
101
|
-
|
|
102
|
-
## Configuring Authentication Services
|
|
103
|
-
|
|
104
|
-
In the server-side database mode, we need an authentication service to distinguish the identities of different users. There are many well-developed authentication solutions in the open-source community. We have integrated two different authentication services to meet the demands of different scenarios, one is Clerk, and the other is NextAuth.
|
|
105
|
-
|
|
106
|
-
### Clerk
|
|
107
|
-
|
|
108
|
-
[Clerk](https://clerk.com?utm_source=lobehub\&utm_medium=docs) is an authentication SaaS service that provides out-of-the-box authentication capabilities with high productization, low integration costs, and a great user experience. For those who offer SaaS products, Clerk is a good choice. Our official [LobeHub Cloud](https://LobeHub.com) uses Clerk as the authentication service.
|
|
109
|
-
|
|
110
|
-
The integration of Clerk is relatively simple, requiring only the configuration of these environment variables:
|
|
111
|
-
|
|
112
|
-
- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`, which can be obtained from the Clerk console
|
|
113
|
-
- `CLERK_WEBHOOK_SECRET`, which is generated by following these instructions: [Configure Clerk Authentication Service](/docs/self-hosting/advanced/auth/clerk#create-and-configure-webhook-in-clerk).
|
|
114
|
-
|
|
115
|
-
<Callout type={'tip'}>
|
|
116
|
-
In Vercel deployment mode, we recommend using Clerk as the authentication service for a better
|
|
117
|
-
user experience.
|
|
118
|
-
</Callout>
|
|
119
|
-
|
|
120
|
-
However, this type of authentication relies on Clerk's official service, so there may be some limitations in certain scenarios:
|
|
121
|
-
|
|
122
|
-
- For example, when using Clerk in China, it may be affected by the network environment.
|
|
123
|
-
- Clerk is not suitable for scenarios that require complete private deployment.
|
|
124
|
-
- It relies on `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, which may not be readily usable with public Docker images.
|
|
125
|
-
|
|
126
|
-
Therefore, for the above scenarios, we also provide NextAuth as an alternative solution.
|
|
127
|
-
|
|
128
|
-
### NextAuth
|
|
129
|
-
|
|
130
|
-
NextAuth is an open-source authentication library that supports multiple identity providers, including Auth0, Cognito, GitHub, Google, Facebook, Apple, Twitter, and more. NextAuth itself provides a complete authentication solution, including user registration, login, password recovery, integration with various identity providers, and more.
|
|
131
|
-
|
|
132
|
-
For information on configuring NextAuth, you can refer to the [Authentication](/docs/self-hosting/advanced/authentication) documentation.
|
|
133
|
-
|
|
134
|
-
<Callout type={'tip'}>
|
|
135
|
-
In the official Docker image `lobehub`, we recommend using NextAuth as the
|
|
136
|
-
authentication service.
|
|
137
|
-
</Callout>
|
|
138
|
-
|
|
139
|
-
## Configuring S3 Storage Service
|
|
140
|
-
|
|
141
|
-
LobeHub has supported multimodal AI conversations since [a long time ago](https://x.com/lobehub/status/1724289575672291782), involving the function of uploading images to large models. In the client-side database solution, image files are stored as binary data directly in the browser's IndexedDB database. However, this solution is not feasible in the server-side database. Storing file-like data directly in Postgres will greatly waste valuable database storage space and slow down computational performance.
|
|
142
|
-
|
|
143
|
-
The best practice in this area is to use a file storage service (S3) to store image files, which is also the storage solution relied upon for subsequent file uploads/knowledge base functions.
|
|
144
|
-
|
|
145
|
-
<Callout type={'info'}>
|
|
146
|
-
In this documentation, S3 refers to a compatible S3 storage solution, which supports the Amazon S3
|
|
147
|
-
API-compatible object storage system. Common examples include Cloudflare R2, Alibaba Cloud OSS,
|
|
148
|
-
and self-deployable RustFS, ceph, all of which support the S3-compatible API.
|
|
149
|
-
</Callout>
|
|
150
|
-
|
|
151
|
-
For detailed configuration guidelines on S3, please refer to [S3 Object Storage](/docs/self-hosting/advanced/s3) for more information.
|
|
152
|
-
|
|
153
|
-
## Getting Started with Deployment
|
|
154
|
-
|
|
155
|
-
The above is a detailed explanation of configuring LobeHub with a server-side database. You can configure it according to your actual situation and then choose a deployment platform that suits you to start deployment:
|
|
156
|
-
|
|
157
|
-
<PlatformCards urlPrefix={'platform'} />
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 使用服务端数据库部署 - 配置数据库、身份验证服务和 S3 存储服务
|
|
3
|
-
description: 本文将介绍服务端数据库版 LobeHub 的部署思路,解释如何配置数据库、身份验证服务和 S3 存储服务。
|
|
4
|
-
tags:
|
|
5
|
-
- 服务端数据库
|
|
6
|
-
- Postgres
|
|
7
|
-
- S3存储服务
|
|
8
|
-
- 数据库配置
|
|
9
|
-
- 身份验证服务
|
|
10
|
-
- 环境变量配置
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# 使用服务端数据库部署
|
|
14
|
-
|
|
15
|
-
LobeHub 默认使用客户端数据库(IndexedDB),同时也支持使用服务端数据库(下简称 DB 版)。LobeHub 采用了 Postgres 作为后端存储数据库。
|
|
16
|
-
|
|
17
|
-
<Callout>
|
|
18
|
-
PostgreSQL 是一种强大的开源关系型数据库管理系统,具备高度扩展性和标准 SQL
|
|
19
|
-
支持。它提供了丰富的数据类型、并发处理、数据完整性、安全性及可编程性,适用于复杂应用和大规模数据管理。
|
|
20
|
-
</Callout>
|
|
21
|
-
|
|
22
|
-
本文将从框架角度介绍在任何一个平台中部署 DB 版 LobeHub 的流程和原理,让你知其然也知其所以然,最后可以根据自己的实际情况进行部署。
|
|
23
|
-
|
|
24
|
-
如你已经熟悉完整原理,可以查看各个平台的部署指南快速开始:
|
|
25
|
-
|
|
26
|
-
<PlatformCards urlPrefix={'platform'} />
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
对于 LobeHub 的 DB 版,正常的部署流程都需要包含三个模块的配置:
|
|
31
|
-
|
|
32
|
-
1. 数据库配置;
|
|
33
|
-
2. 身份验证服务配置;
|
|
34
|
-
3. S3 存储服务配置。
|
|
35
|
-
|
|
36
|
-
## 配置数据库
|
|
37
|
-
|
|
38
|
-
在部署之前,请确保你已经准备好 Postgres 数据库实例,你可以选择以下任一实例:
|
|
39
|
-
|
|
40
|
-
- `A.` 使用 Vercel / Neon 等 Serverless Postgres 实例;
|
|
41
|
-
- `B.` 使用 Docker / Railway / Zeabur 等自部署 Postgres 实例,下统称 Node Postgres 实例;
|
|
42
|
-
|
|
43
|
-
<Callout>两者的配置方式在环境变量的取值上会略有一点区别,其他方面是一样的。</Callout>
|
|
44
|
-
|
|
45
|
-
同时,由于我们支持了文件对话 / 知识库对话的能力,因此我们需要为 Postgres 安装 `pgvector` 插件,该插件提供了向量搜索的能力,是 LobeHub 实现 RAG 的重要构件之一。
|
|
46
|
-
|
|
47
|
-
<Steps>
|
|
48
|
-
### `NEXT_PUBLIC_SERVICE_MODE`
|
|
49
|
-
|
|
50
|
-
LobeHub 同时支持了客户端数据库和服务端数据库,因此我们提供了一个环境变量用于切换模式,这个变量为 `NEXT_PUBLIC_SERVICE_MODE`,该值默认为 `client`。
|
|
51
|
-
|
|
52
|
-
针对服务端数据库部署场景,你需要将 `NEXT_PUBLIC_SERVICE_MODE` 设置为 `server`。
|
|
53
|
-
|
|
54
|
-
<Callout type={'info'}>
|
|
55
|
-
在官方的 `lobehub` Docker 镜像中,已经默认将该环境变量设为 `server`,因此如果你使用
|
|
56
|
-
Docker 镜像部署,则无需再配置该环境变量。
|
|
57
|
-
</Callout>
|
|
58
|
-
|
|
59
|
-
<Callout type={'tip'}>
|
|
60
|
-
由于 `NEXT_PUBLIC` 开头的环境变量是在前端代码中生效的,而因此无法通过容器运行时注入进行修改。 (`next.js`的参考文档 [Configuring: Environment Variables | Next.js (nextjs.org)](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables) ) 这也是为什么我们选择再打一个 DB 版镜像的原因。
|
|
61
|
-
|
|
62
|
-
如果你需要在 Docker 部署中修改 `NEXT_PUBLIC` 前缀的变量,你必须自行构建镜像,在 build 时就把自己的 `NEXT_PUBLIC` 开头的环境变量打进去。
|
|
63
|
-
</Callout>
|
|
64
|
-
|
|
65
|
-
### `DATABASE_URL`
|
|
66
|
-
|
|
67
|
-
配置数据库,核心是添加 `DATABASE_URL` 环境变量,将你准备好的 Postgres 数据库连接 URL 填入其中。数据库连接 URL 的通常格式为 `postgres://username:password@host:port/database`。
|
|
68
|
-
|
|
69
|
-
<Callout type={'info'}>
|
|
70
|
-
如果希望连接数据库时启用 SSL
|
|
71
|
-
,请自行参考[文档](https://stackoverflow.com/questions/14021998/using-psql-to-connect-to-postgresql-in-ssl-mode)进行设置
|
|
72
|
-
</Callout>
|
|
73
|
-
|
|
74
|
-
### `DATABASE_DRIVER`
|
|
75
|
-
|
|
76
|
-
`DATABASE_DRIVER` 环境变量用于区分两种 Postgres 数据库实例,`DATABASE_DRIVER` 的取值为 `node` 或 `neon`。
|
|
77
|
-
|
|
78
|
-
为提升部署便捷性,我们根据不同的平台特点设置了默认值:
|
|
79
|
-
|
|
80
|
-
- 在 Vercel 平台下,`DATABASE_DRIVER` 默认为 `neon`;
|
|
81
|
-
- 在我们提供的 Docker 镜像 `lobehub` 中,`DATABASE_DRIVER` 默认为 `node`。
|
|
82
|
-
|
|
83
|
-
因此如果你采用了以下标准的部署方式,你无需手动配置 `DATABASE_DRIVER` 环境变量:
|
|
84
|
-
|
|
85
|
-
- Vercel + Serverless Postgres
|
|
86
|
-
- Docker 镜像 + Node Postgres
|
|
87
|
-
|
|
88
|
-
### `KEY_VAULTS_SECRET`
|
|
89
|
-
|
|
90
|
-
考虑到用户会存储自己的 API Key 和 baseURL 等敏感信息到数据库中,因此我们需要一个密钥来加密这些信息,避免数据库被爆破 / 脱库时这些关键信息被泄露。 因此有了 `KEY_VAULTS_SECRET` 环境变量,用于加密用户存储的 apikey 等敏感信息。
|
|
91
|
-
|
|
92
|
-
<Callout type={'info'}>
|
|
93
|
-
你可以使用 `openssl rand -base64 32` 生成一个随机的 32 位字符串作为 `KEY_VAULTS_SECRET` 的值。
|
|
94
|
-
</Callout>
|
|
95
|
-
</Steps>
|
|
96
|
-
|
|
97
|
-
## 配置身份验证服务
|
|
98
|
-
|
|
99
|
-
在服务端数据库模式下,我们要为不同用户区分身份,因此需要一个身份验证服务。开源社区中已经存在较多完善的身份验证解决方案。我们在实现过程中集成了两种不同的身份验证服务,用于满足不同场景的诉求,一种是 Clerk ,另外一种是 NextAuth。
|
|
100
|
-
|
|
101
|
-
### Clerk
|
|
102
|
-
|
|
103
|
-
[Clerk](https://clerk.com?utm_source=lobehub\&utm_medium=docs) 是一个身份验证 SaaS 服务,提供了开箱即用的身份验证能力,产品化程度很高,集成成本较低,体验很好。对于提供 SaaS 化产品的诉求来说,Clerk 是一个不错的选择。我们官方提供的 [LobeHub Cloud](https://LobeHub.com),就是使用了 Clerk 作为身份验证服务。
|
|
104
|
-
|
|
105
|
-
Clerk 的集成也相对简单,只需要配置 `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` 、 `CLERK_SECRET_KEY` 和 `CLERK_WEBHOOK_SECRET` 环境变量即可,这三个环境变量可以在 Clerk 控制台中获取。
|
|
106
|
-
|
|
107
|
-
<Callout type={'tip'}>
|
|
108
|
-
在 Vercel 部署模式下,我们推荐使用 Clerk 作为身份验证服务,可以获得更好的用户体验。
|
|
109
|
-
</Callout>
|
|
110
|
-
|
|
111
|
-
但是这种身份验证依赖了 Clerk 官方的服务,因此在一些场景下可能会有一些限制:
|
|
112
|
-
|
|
113
|
-
- 比如在国内使用 Clerk 时,可能会受到网络环境的影响;
|
|
114
|
-
- 需要完全私有化部署的场景下,Clerk 并不适用;
|
|
115
|
-
- 必须依赖 `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`,对于公共 Docker 镜像无法开箱即用;
|
|
116
|
-
|
|
117
|
-
因此针对上述场景,我们也提供了 NextAuth 作为备选方案。
|
|
118
|
-
|
|
119
|
-
### NextAuth
|
|
120
|
-
|
|
121
|
-
NextAuth 是一个开源的身份验证库,支持多种身份验证提供商,包括 Auth0、Cognito、GitHub、Google、Facebook、Apple、Twitter 等。NextAuth 本身提供了一套完整的身份验证解决方案,包括用户注册、登录、密码找回、多种身份验证提供商的集成等。
|
|
122
|
-
|
|
123
|
-
关于 NextAuth 的配置,你可以参考 [身份验证](/zh/docs/self-hosting/advanced/authentication) 的文档获取更多信息。
|
|
124
|
-
|
|
125
|
-
<Callout type={'tip'}>
|
|
126
|
-
在官方的 Docker 镜像 `lobehub` 中,我们推荐使用 NextAuth 作为身份验证服务。
|
|
127
|
-
</Callout>
|
|
128
|
-
|
|
129
|
-
## 配置 S3 存储服务
|
|
130
|
-
|
|
131
|
-
LobeHub 在 [很早以前](https://x.com/lobehub/status/1724289575672291782) 就支持了多模态的 AI 会话,其中涉及到图片上传给大模型的功能。在客户端数据库方案中,图片文件直接以二进制数据存储在浏览器 IndexedDB 数据库,但在服务端数据库中这个方案并不可行。因为在 Postgres 中直接存储文件类二进制数据会大大浪费宝贵的数据库存储空间,并拖慢计算性能。
|
|
132
|
-
|
|
133
|
-
这块最佳实践是使用文件存储服务(S3)来存储图片文件,同时 S3 也是文件上传 / 知识库功能所依赖的大容量静态文件存储方案。
|
|
134
|
-
|
|
135
|
-
<Callout type={'info'}>
|
|
136
|
-
在本文档库中,S3 所指代的是指兼容 S3 存储方案,即支持 Amazon S3 API 的对象存储系统,常见例如
|
|
137
|
-
Cloudflare R2 、阿里云 OSS,可以自部署的 RustFS、ceph 等均支持 S3 兼容 API。
|
|
138
|
-
</Callout>
|
|
139
|
-
|
|
140
|
-
关于 S3 的详细配置指南,请参阅 [S3 对象存储](/zh/docs/self-hosting/advanced/s3) 了解详情。
|
|
141
|
-
|
|
142
|
-
## 开始部署
|
|
143
|
-
|
|
144
|
-
以上就是关于服务端数据库版 LobeHub 的配置详解,你可以根据自己的实际情况进行配置,然后选择适合自己的部署平台开始部署:
|
|
145
|
-
|
|
146
|
-
<PlatformCards urlPrefix={'platform'} />
|