@lobehub/lobehub 2.1.19 → 2.1.21
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/.github/workflows/release.yml +1 -0
- package/.github/workflows/sync-main-to-dev.yaml +42 -0
- package/.vscode/settings.json +2 -0
- package/CHANGELOG.md +162 -0
- package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +14 -3
- package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +60 -4
- package/changelog/v2.json +18 -0
- package/docs/development/basic/add-new-image-model.mdx +7 -7
- package/docs/development/basic/add-new-image-model.zh-CN.mdx +7 -7
- package/docs/development/basic/architecture.mdx +85 -21
- package/docs/development/basic/architecture.zh-CN.mdx +81 -19
- package/docs/development/basic/chat-api.mdx +385 -322
- package/docs/development/basic/chat-api.zh-CN.mdx +347 -320
- package/docs/development/basic/contributing-guidelines.mdx +20 -3
- package/docs/development/basic/contributing-guidelines.zh-CN.mdx +20 -3
- package/docs/development/basic/feature-development.mdx +102 -214
- package/docs/development/basic/feature-development.zh-CN.mdx +102 -214
- package/docs/development/basic/folder-structure.mdx +189 -80
- package/docs/development/basic/folder-structure.zh-CN.mdx +187 -80
- package/docs/development/basic/resources.mdx +19 -0
- package/docs/development/basic/resources.zh-CN.mdx +19 -0
- package/docs/development/start.mdx +53 -63
- package/docs/development/start.zh-CN.mdx +48 -63
- package/locales/ar/auth.json +1 -0
- package/locales/ar/chat.json +6 -0
- package/locales/ar/discover.json +9 -0
- package/locales/ar/error.json +1 -0
- package/locales/ar/modelProvider.json +2 -0
- package/locales/ar/models.json +99 -26
- package/locales/ar/plugin.json +1 -0
- package/locales/ar/providers.json +1 -0
- package/locales/bg-BG/auth.json +1 -0
- package/locales/bg-BG/chat.json +6 -0
- package/locales/bg-BG/discover.json +9 -0
- package/locales/bg-BG/error.json +1 -0
- package/locales/bg-BG/modelProvider.json +2 -0
- package/locales/bg-BG/models.json +33 -24
- package/locales/bg-BG/plugin.json +1 -0
- package/locales/bg-BG/providers.json +1 -0
- package/locales/de-DE/auth.json +1 -0
- package/locales/de-DE/chat.json +6 -0
- package/locales/de-DE/discover.json +9 -0
- package/locales/de-DE/error.json +1 -0
- package/locales/de-DE/modelProvider.json +2 -0
- package/locales/de-DE/models.json +25 -24
- package/locales/de-DE/plugin.json +1 -0
- package/locales/de-DE/providers.json +1 -0
- package/locales/en-US/chat.json +4 -0
- package/locales/en-US/error.json +1 -0
- package/locales/en-US/modelProvider.json +2 -0
- package/locales/en-US/models.json +28 -27
- package/locales/en-US/providers.json +1 -0
- package/locales/es-ES/auth.json +1 -0
- package/locales/es-ES/chat.json +6 -0
- package/locales/es-ES/discover.json +9 -0
- package/locales/es-ES/error.json +1 -0
- package/locales/es-ES/modelProvider.json +2 -0
- package/locales/es-ES/models.json +23 -22
- package/locales/es-ES/plugin.json +1 -0
- package/locales/es-ES/providers.json +1 -0
- package/locales/fa-IR/auth.json +1 -0
- package/locales/fa-IR/chat.json +6 -0
- package/locales/fa-IR/discover.json +9 -0
- package/locales/fa-IR/error.json +1 -0
- package/locales/fa-IR/modelProvider.json +2 -0
- package/locales/fa-IR/models.json +58 -25
- package/locales/fa-IR/plugin.json +1 -0
- package/locales/fa-IR/providers.json +1 -0
- package/locales/fr-FR/auth.json +1 -0
- package/locales/fr-FR/chat.json +6 -0
- package/locales/fr-FR/discover.json +9 -0
- package/locales/fr-FR/error.json +1 -0
- package/locales/fr-FR/modelProvider.json +2 -0
- package/locales/fr-FR/models.json +37 -25
- package/locales/fr-FR/plugin.json +1 -0
- package/locales/fr-FR/providers.json +1 -0
- package/locales/it-IT/auth.json +1 -0
- package/locales/it-IT/chat.json +6 -0
- package/locales/it-IT/discover.json +9 -0
- package/locales/it-IT/error.json +1 -0
- package/locales/it-IT/modelProvider.json +2 -0
- package/locales/it-IT/models.json +81 -20
- package/locales/it-IT/plugin.json +1 -0
- package/locales/it-IT/providers.json +1 -0
- package/locales/ja-JP/auth.json +1 -0
- package/locales/ja-JP/chat.json +6 -0
- package/locales/ja-JP/discover.json +9 -0
- package/locales/ja-JP/error.json +1 -0
- package/locales/ja-JP/modelProvider.json +2 -0
- package/locales/ja-JP/models.json +100 -24
- package/locales/ja-JP/plugin.json +1 -0
- package/locales/ja-JP/providers.json +1 -0
- package/locales/ko-KR/auth.json +1 -0
- package/locales/ko-KR/chat.json +6 -0
- package/locales/ko-KR/discover.json +9 -0
- package/locales/ko-KR/error.json +1 -0
- package/locales/ko-KR/modelProvider.json +2 -0
- package/locales/ko-KR/models.json +124 -21
- package/locales/ko-KR/plugin.json +1 -0
- package/locales/ko-KR/providers.json +1 -0
- package/locales/nl-NL/auth.json +1 -0
- package/locales/nl-NL/chat.json +6 -0
- package/locales/nl-NL/discover.json +9 -0
- package/locales/nl-NL/error.json +1 -0
- package/locales/nl-NL/modelProvider.json +2 -0
- package/locales/nl-NL/models.json +103 -21
- package/locales/nl-NL/plugin.json +1 -0
- package/locales/nl-NL/providers.json +1 -0
- package/locales/pl-PL/auth.json +1 -0
- package/locales/pl-PL/chat.json +6 -0
- package/locales/pl-PL/discover.json +9 -0
- package/locales/pl-PL/error.json +1 -0
- package/locales/pl-PL/modelProvider.json +2 -0
- package/locales/pl-PL/models.json +36 -27
- package/locales/pl-PL/plugin.json +1 -0
- package/locales/pl-PL/providers.json +1 -0
- package/locales/pt-BR/auth.json +1 -0
- package/locales/pt-BR/chat.json +6 -0
- package/locales/pt-BR/discover.json +9 -0
- package/locales/pt-BR/error.json +1 -0
- package/locales/pt-BR/modelProvider.json +2 -0
- package/locales/pt-BR/models.json +26 -25
- package/locales/pt-BR/plugin.json +1 -0
- package/locales/pt-BR/providers.json +1 -0
- package/locales/ru-RU/auth.json +1 -0
- package/locales/ru-RU/chat.json +6 -0
- package/locales/ru-RU/discover.json +9 -0
- package/locales/ru-RU/error.json +1 -0
- package/locales/ru-RU/modelProvider.json +2 -0
- package/locales/ru-RU/models.json +83 -25
- package/locales/ru-RU/plugin.json +1 -0
- package/locales/ru-RU/providers.json +1 -0
- package/locales/tr-TR/auth.json +1 -0
- package/locales/tr-TR/chat.json +6 -0
- package/locales/tr-TR/discover.json +9 -0
- package/locales/tr-TR/error.json +1 -0
- package/locales/tr-TR/modelProvider.json +2 -0
- package/locales/tr-TR/models.json +89 -27
- package/locales/tr-TR/plugin.json +1 -0
- package/locales/tr-TR/providers.json +1 -0
- package/locales/vi-VN/auth.json +1 -0
- package/locales/vi-VN/chat.json +6 -0
- package/locales/vi-VN/discover.json +9 -0
- package/locales/vi-VN/error.json +1 -0
- package/locales/vi-VN/modelProvider.json +2 -0
- package/locales/vi-VN/models.json +78 -23
- package/locales/vi-VN/plugin.json +1 -0
- package/locales/vi-VN/providers.json +1 -0
- package/locales/zh-CN/chat.json +4 -4
- package/locales/zh-CN/error.json +1 -0
- package/locales/zh-CN/modelProvider.json +2 -2
- package/locales/zh-CN/models.json +26 -25
- package/locales/zh-CN/providers.json +1 -0
- package/locales/zh-TW/auth.json +1 -0
- package/locales/zh-TW/chat.json +6 -0
- package/locales/zh-TW/discover.json +9 -0
- package/locales/zh-TW/error.json +1 -0
- package/locales/zh-TW/modelProvider.json +2 -0
- package/locales/zh-TW/models.json +28 -27
- package/locales/zh-TW/plugin.json +1 -0
- package/locales/zh-TW/providers.json +1 -0
- package/package.json +10 -8
- package/packages/agent-runtime/src/core/__tests__/defaultSecurityBlacklist.test.ts +870 -0
- package/packages/database/migrations/0077_add_agent_skills.sql +26 -0
- package/packages/database/migrations/meta/0077_snapshot.json +11473 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/models/__tests__/agentSkill.test.ts +302 -0
- package/packages/database/src/models/agentSkill.ts +152 -0
- package/packages/database/src/repositories/compression/index.test.ts +1 -1
- package/packages/database/src/schemas/agentSkill.ts +71 -0
- package/packages/database/src/schemas/index.ts +1 -0
- package/packages/database/src/utils/idGenerator.ts +1 -0
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +1 -0
- package/packages/model-runtime/src/core/streams/bedrock/claude.ts +1 -1
- package/packages/model-runtime/src/core/streams/bedrock/llama.test.ts +4 -8
- package/packages/model-runtime/src/core/streams/bedrock/llama.ts +1 -1
- package/packages/model-runtime/src/core/streams/ollama.test.ts +6 -12
- package/packages/model-runtime/src/core/streams/ollama.ts +1 -1
- package/packages/model-runtime/src/core/streams/protocol.ts +1 -1
- package/packages/model-runtime/src/core/streams/qwen.test.ts +1 -1
- package/packages/types/src/index.ts +1 -0
- package/packages/types/src/session/sessionGroup.ts +5 -2
- package/packages/types/src/skill/index.ts +166 -0
- package/src/app/(backend)/api/agent/run/route.ts +2 -2
- package/src/app/(backend)/api/agent/stream/route.ts +15 -15
- package/src/app/(backend)/oidc/consent/route.ts +9 -9
- package/src/app/(backend)/trpc/async/[trpc]/route.ts +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/Modals/ConfigGroupModal/GroupItem.tsx +2 -2
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/Modals/ConfigGroupModal/index.tsx +3 -3
- package/src/app/[variants]/(mobile)/chat/features/Topic/index.tsx +1 -1
- package/src/app/robots.tsx +2 -2
- package/src/app/sitemap.tsx +8 -8
- package/src/components/AnimatedCollapsed/index.tsx +1 -1
- package/src/components/DragUpload/useDragUpload.tsx +2 -2
- package/src/components/InvalidAPIKey/ComfyUIForm.tsx +1 -1
- package/src/components/MCPStdioCommandInput/index.tsx +2 -2
- package/src/components/StatisticCard/growthPercentage.tsx +1 -1
- package/src/components/TipGuide/index.tsx +10 -10
- package/src/libs/next/proxy/define-config.ts +4 -0
- package/src/locales/default/error.ts +2 -0
- package/src/server/routers/lambda/market/agentGroup.ts +2 -4
- package/src/server/services/oidc/oidcProvider.ts +3 -3
- package/src/server/sitemap.ts +4 -4
- package/src/store/chat/slices/topic/action.test.ts +190 -0
- package/src/store/chat/slices/topic/action.ts +10 -8
- package/src/store/file/slices/upload/action.ts +89 -76
- package/src/store/home/slices/sidebarUI/action.ts +2 -2
- package/src/utils/unzipFile.test.ts +355 -0
- package/tsconfig.json +1 -0
- package/docs/development/basic/feature-development-frontend.mdx +0 -144
- package/docs/development/basic/feature-development-frontend.zh-CN.mdx +0 -142
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['unicorn/text-encoding-identifier-case'] = 0;
|
|
23
24
|
config.rules['@typescript-eslint/no-use-before-define'] = 0;
|
|
24
25
|
// FIXME: Linting error in src/app/[variants]/(main)/chat/features/Migration/DBReader.ts, the fundamental solution should be upgrading typescript-eslint
|
|
25
26
|
config.rules['@typescript-eslint/no-useless-constructor'] = 0;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: 🔄 Branch Synchronization
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
sync-branches:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout repository
|
|
13
|
+
uses: actions/checkout@v6
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
|
|
17
|
+
- name: Set up Git
|
|
18
|
+
run: |
|
|
19
|
+
git config --global user.name 'GitHub Actions'
|
|
20
|
+
git config --global user.email 'actions@github.com'
|
|
21
|
+
|
|
22
|
+
- name: Prepare sync branch
|
|
23
|
+
id: branch
|
|
24
|
+
run: |
|
|
25
|
+
echo "SYNC_BRANCH_MAIN_DEV=sync/main-to-dev-$(date +'%Y%m%d')" >> $GITHUB_ENV
|
|
26
|
+
|
|
27
|
+
- name: Sync main to dev
|
|
28
|
+
if: github.ref == 'refs/heads/main'
|
|
29
|
+
run: |
|
|
30
|
+
# Sync main to dev
|
|
31
|
+
git checkout main
|
|
32
|
+
SYNC_BRANCH_DEV=${{ env.SYNC_BRANCH_MAIN_DEV }}
|
|
33
|
+
git checkout -B $SYNC_BRANCH_DEV
|
|
34
|
+
DIFF=$(git diff origin/dev...)
|
|
35
|
+
if [ -z "$DIFF" ]; then
|
|
36
|
+
echo "No changes to sync"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
git push origin $SYNC_BRANCH_DEV -f
|
|
40
|
+
gh pr create --base dev --head $SYNC_BRANCH_DEV --title "Sync main branch to dev branch" --body "Automatic sync" || exit 0
|
|
41
|
+
env:
|
|
42
|
+
GH_TOKEN: ${{ github.token }}
|
package/.vscode/settings.json
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// support mdx
|
|
25
25
|
"mdx"
|
|
26
26
|
],
|
|
27
|
+
"mdx.server.enable": false,
|
|
27
28
|
"npm.packageManager": "pnpm",
|
|
28
29
|
"search.exclude": {
|
|
29
30
|
"**/node_modules": true,
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
// make stylelint work with tsx antd-style css template string
|
|
52
53
|
"typescriptreact"
|
|
53
54
|
],
|
|
55
|
+
"typescript.tsdk": "node_modules/typescript/lib",
|
|
54
56
|
"vitest.maximumConfigs": 20,
|
|
55
57
|
"workbench.editor.customLabels.patterns": {
|
|
56
58
|
"**/app/**/[[]*[]]/[[]*[]]/page.tsx": "${dirname(2)}/${dirname(1)}/${dirname} • page component",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,168 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 2.1.21](https://github.com/lobehub/lobe-chat/compare/v2.1.20...v2.1.21)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-02-08**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Add end-user info on OpenAI Responses API call, enable vertical scrolling for topic list on mobile.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Add end-user info on OpenAI Responses API call, closes [#12134](https://github.com/lobehub/lobe-chat/issues/12134) ([72a85ac](https://github.com/lobehub/lobe-chat/commit/72a85ac))
|
|
21
|
+
- **misc**: Enable vertical scrolling for topic list on mobile, closes [#12157](https://github.com/lobehub/lobe-chat/issues/12157) [lobehub/lobe-chat#12029](https://github.com/lobehub/lobe-chat/issues/12029) ([bd4e253](https://github.com/lobehub/lobe-chat/commit/bd4e253))
|
|
22
|
+
|
|
23
|
+
</details>
|
|
24
|
+
|
|
25
|
+
<div align="right">
|
|
26
|
+
|
|
27
|
+
[](#readme-top)
|
|
28
|
+
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
### [Version 2.1.21](https://github.com/lobehub/lobe-chat/compare/v2.1.20...v2.1.21)
|
|
32
|
+
|
|
33
|
+
<sup>Released on **2026-02-08**</sup>
|
|
34
|
+
|
|
35
|
+
<br/>
|
|
36
|
+
|
|
37
|
+
<details>
|
|
38
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
39
|
+
|
|
40
|
+
</details>
|
|
41
|
+
|
|
42
|
+
<div align="right">
|
|
43
|
+
|
|
44
|
+
[](#readme-top)
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
### [Version 2.1.21](https://github.com/lobehub/lobe-chat/compare/v2.1.20...v2.1.21)
|
|
49
|
+
|
|
50
|
+
<sup>Released on **2026-02-08**</sup>
|
|
51
|
+
|
|
52
|
+
<br/>
|
|
53
|
+
|
|
54
|
+
<details>
|
|
55
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
56
|
+
|
|
57
|
+
</details>
|
|
58
|
+
|
|
59
|
+
<div align="right">
|
|
60
|
+
|
|
61
|
+
[](#readme-top)
|
|
62
|
+
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
### [Version 2.1.20](https://github.com/lobehub/lobe-chat/compare/v2.1.19...v2.1.20)
|
|
66
|
+
|
|
67
|
+
<sup>Released on **2026-02-08**</sup>
|
|
68
|
+
|
|
69
|
+
#### 🐛 Bug Fixes
|
|
70
|
+
|
|
71
|
+
- **misc**: Add api/version and api/desktop to public routes, show notification when file upload fails due to storage plan limit.
|
|
72
|
+
|
|
73
|
+
<br/>
|
|
74
|
+
|
|
75
|
+
<details>
|
|
76
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
77
|
+
|
|
78
|
+
#### What's fixed
|
|
79
|
+
|
|
80
|
+
- **misc**: Add api/version and api/desktop to public routes, closes [#12194](https://github.com/lobehub/lobe-chat/issues/12194) ([ea81cd4](https://github.com/lobehub/lobe-chat/commit/ea81cd4))
|
|
81
|
+
- **misc**: Show notification when file upload fails due to storage plan limit, closes [#12176](https://github.com/lobehub/lobe-chat/issues/12176) ([f26d0df](https://github.com/lobehub/lobe-chat/commit/f26d0df))
|
|
82
|
+
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
<div align="right">
|
|
86
|
+
|
|
87
|
+
[](#readme-top)
|
|
88
|
+
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
### [Version 2.1.20](https://github.com/lobehub/lobe-chat/compare/v2.1.19...v2.1.20)
|
|
92
|
+
|
|
93
|
+
<sup>Released on **2026-02-08**</sup>
|
|
94
|
+
|
|
95
|
+
#### 🐛 Bug Fixes
|
|
96
|
+
|
|
97
|
+
- **misc**: Add api/version and api/desktop to public routes, show notification when file upload fails due to storage plan limit.
|
|
98
|
+
|
|
99
|
+
<br/>
|
|
100
|
+
|
|
101
|
+
<details>
|
|
102
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
103
|
+
|
|
104
|
+
#### What's fixed
|
|
105
|
+
|
|
106
|
+
- **misc**: Add api/version and api/desktop to public routes, closes [#12194](https://github.com/lobehub/lobe-chat/issues/12194) ([ea81cd4](https://github.com/lobehub/lobe-chat/commit/ea81cd4))
|
|
107
|
+
- **misc**: Show notification when file upload fails due to storage plan limit, closes [#12176](https://github.com/lobehub/lobe-chat/issues/12176) ([f26d0df](https://github.com/lobehub/lobe-chat/commit/f26d0df))
|
|
108
|
+
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
<div align="right">
|
|
112
|
+
|
|
113
|
+
[](#readme-top)
|
|
114
|
+
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
### [Version 2.1.20](https://github.com/lobehub/lobe-chat/compare/v2.1.19...v2.1.20)
|
|
118
|
+
|
|
119
|
+
<sup>Released on **2026-02-07**</sup>
|
|
120
|
+
|
|
121
|
+
#### 🐛 Bug Fixes
|
|
122
|
+
|
|
123
|
+
- **misc**: Show notification when file upload fails due to storage plan limit.
|
|
124
|
+
|
|
125
|
+
<br/>
|
|
126
|
+
|
|
127
|
+
<details>
|
|
128
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
129
|
+
|
|
130
|
+
#### What's fixed
|
|
131
|
+
|
|
132
|
+
- **misc**: Show notification when file upload fails due to storage plan limit, closes [#12176](https://github.com/lobehub/lobe-chat/issues/12176) ([f26d0df](https://github.com/lobehub/lobe-chat/commit/f26d0df))
|
|
133
|
+
|
|
134
|
+
</details>
|
|
135
|
+
|
|
136
|
+
<div align="right">
|
|
137
|
+
|
|
138
|
+
[](#readme-top)
|
|
139
|
+
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
### [Version 2.1.20](https://github.com/lobehub/lobe-chat/compare/v2.1.19...v2.1.20)
|
|
143
|
+
|
|
144
|
+
<sup>Released on **2026-02-07**</sup>
|
|
145
|
+
|
|
146
|
+
#### 🐛 Bug Fixes
|
|
147
|
+
|
|
148
|
+
- **misc**: Show notification when file upload fails due to storage plan limit.
|
|
149
|
+
|
|
150
|
+
<br/>
|
|
151
|
+
|
|
152
|
+
<details>
|
|
153
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
154
|
+
|
|
155
|
+
#### What's fixed
|
|
156
|
+
|
|
157
|
+
- **misc**: Show notification when file upload fails due to storage plan limit, closes [#12176](https://github.com/lobehub/lobe-chat/issues/12176) ([f26d0df](https://github.com/lobehub/lobe-chat/commit/f26d0df))
|
|
158
|
+
|
|
159
|
+
</details>
|
|
160
|
+
|
|
161
|
+
<div align="right">
|
|
162
|
+
|
|
163
|
+
[](#readme-top)
|
|
164
|
+
|
|
165
|
+
</div>
|
|
166
|
+
|
|
5
167
|
### [Version 2.1.19](https://github.com/lobehub/lobe-chat/compare/v2.1.18...v2.1.19)
|
|
6
168
|
|
|
7
169
|
<sup>Released on **2026-02-06**</sup>
|
|
@@ -16,15 +16,26 @@ import { ControllerModule, IpcMethod } from './index';
|
|
|
16
16
|
const logger = createLogger('controllers:ShellCommandCtr');
|
|
17
17
|
|
|
18
18
|
// Maximum output length to prevent context explosion
|
|
19
|
-
const MAX_OUTPUT_LENGTH =
|
|
19
|
+
const MAX_OUTPUT_LENGTH = 80_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strip ANSI escape codes from terminal output
|
|
23
|
+
*/
|
|
24
|
+
// eslint-disable-next-line no-control-regex
|
|
25
|
+
const ANSI_REGEX = /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
26
|
+
const stripAnsi = (str: string): string => str.replaceAll(ANSI_REGEX, '');
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* Truncate string to max length with ellipsis indicator
|
|
23
30
|
*/
|
|
24
31
|
const truncateOutput = (str: string, maxLength: number = MAX_OUTPUT_LENGTH): string => {
|
|
25
|
-
|
|
32
|
+
const cleaned = stripAnsi(str);
|
|
33
|
+
if (cleaned.length <= maxLength) return cleaned;
|
|
26
34
|
return (
|
|
27
|
-
|
|
35
|
+
cleaned.slice(0, maxLength) +
|
|
36
|
+
'\n... [truncated, ' +
|
|
37
|
+
(cleaned.length - maxLength) +
|
|
38
|
+
' more characters]'
|
|
28
39
|
);
|
|
29
40
|
};
|
|
30
41
|
|
|
@@ -193,6 +193,62 @@ describe('ShellCommandCtr', () => {
|
|
|
193
193
|
expect(result.stderr).toBe('error message\n');
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
it('should strip ANSI escape codes from output', async () => {
|
|
197
|
+
let exitCallback: (code: number) => void;
|
|
198
|
+
let stdoutCallback: (data: Buffer) => void;
|
|
199
|
+
let stderrCallback: (data: Buffer) => void;
|
|
200
|
+
|
|
201
|
+
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
|
|
202
|
+
if (event === 'exit') {
|
|
203
|
+
exitCallback = callback;
|
|
204
|
+
setTimeout(() => exitCallback(0), 10);
|
|
205
|
+
}
|
|
206
|
+
return mockChildProcess;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
|
|
210
|
+
if (event === 'data') {
|
|
211
|
+
stdoutCallback = callback;
|
|
212
|
+
// Simulate output with ANSI color codes
|
|
213
|
+
setTimeout(
|
|
214
|
+
() =>
|
|
215
|
+
stdoutCallback(
|
|
216
|
+
Buffer.from(
|
|
217
|
+
'\x1b[38;5;250m███████╗\x1b[0m\n\x1b[1;32mSuccess\x1b[0m\n\x1b[31mError\x1b[0m',
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
5,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return mockChildProcess.stdout;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
mockChildProcess.stderr.on.mockImplementation((event: string, callback: any) => {
|
|
227
|
+
if (event === 'data') {
|
|
228
|
+
stderrCallback = callback;
|
|
229
|
+
setTimeout(
|
|
230
|
+
() => stderrCallback(Buffer.from('\x1b[33mwarning:\x1b[0m something happened')),
|
|
231
|
+
5,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return mockChildProcess.stderr;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = await shellCommandCtr.handleRunCommand({
|
|
238
|
+
command: 'npx skills find react',
|
|
239
|
+
description: 'search skills',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(result.success).toBe(true);
|
|
243
|
+
// ANSI codes should be stripped
|
|
244
|
+
expect(result.stdout).not.toContain('\x1b[');
|
|
245
|
+
expect(result.stdout).toContain('███████╗');
|
|
246
|
+
expect(result.stdout).toContain('Success');
|
|
247
|
+
expect(result.stdout).toContain('Error');
|
|
248
|
+
expect(result.stderr).not.toContain('\x1b[');
|
|
249
|
+
expect(result.stderr).toContain('warning: something happened');
|
|
250
|
+
});
|
|
251
|
+
|
|
196
252
|
it('should truncate long output to prevent context explosion', async () => {
|
|
197
253
|
let exitCallback: (code: number) => void;
|
|
198
254
|
let stdoutCallback: (data: Buffer) => void;
|
|
@@ -208,8 +264,8 @@ describe('ShellCommandCtr', () => {
|
|
|
208
264
|
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
|
|
209
265
|
if (event === 'data') {
|
|
210
266
|
stdoutCallback = callback;
|
|
211
|
-
// Simulate very long output (
|
|
212
|
-
const longOutput = 'x'.repeat(
|
|
267
|
+
// Simulate very long output (100k characters, exceeding 80k MAX_OUTPUT_LENGTH)
|
|
268
|
+
const longOutput = 'x'.repeat(100_000);
|
|
213
269
|
setTimeout(() => stdoutCallback(Buffer.from(longOutput)), 5);
|
|
214
270
|
}
|
|
215
271
|
return mockChildProcess.stdout;
|
|
@@ -223,8 +279,8 @@ describe('ShellCommandCtr', () => {
|
|
|
223
279
|
});
|
|
224
280
|
|
|
225
281
|
expect(result.success).toBe(true);
|
|
226
|
-
// Output should be truncated to
|
|
227
|
-
expect(result.stdout!.length).toBeLessThan(
|
|
282
|
+
// Output should be truncated to 80k + truncation message
|
|
283
|
+
expect(result.stdout!.length).toBeLessThan(100_000);
|
|
228
284
|
expect(result.stdout).toContain('truncated');
|
|
229
285
|
expect(result.stdout).toContain('more characters');
|
|
230
286
|
});
|
package/changelog/v2.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"fixes": [
|
|
5
|
+
"Add end-user info on OpenAI Responses API call, enable vertical scrolling for topic list on mobile."
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"date": "2026-02-08",
|
|
9
|
+
"version": "2.1.21"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"children": {
|
|
13
|
+
"fixes": [
|
|
14
|
+
"Add api/version and api/desktop to public routes, show notification when file upload fails due to storage plan limit."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"date": "2026-02-08",
|
|
18
|
+
"version": "2.1.20"
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"children": {
|
|
4
22
|
"fixes": [
|
|
@@ -15,7 +15,7 @@ tags:
|
|
|
15
15
|
|
|
16
16
|
## Parameter Standardization
|
|
17
17
|
|
|
18
|
-
All image generation models must use the standard parameters defined in `src/
|
|
18
|
+
All image generation models must use the standard parameters defined in `packages/model-bank/src/standard-parameters/index.ts`. This ensures parameter consistency across different Providers, creating a more unified user experience.
|
|
19
19
|
|
|
20
20
|
**Supported Standard Parameters**:
|
|
21
21
|
|
|
@@ -32,7 +32,7 @@ All image generation models must use the standard parameters defined in `src/lib
|
|
|
32
32
|
|
|
33
33
|
These models can be requested using the OpenAI SDK, with request parameters and return values consistent with DALL-E and GPT-Image-X series.
|
|
34
34
|
|
|
35
|
-
Taking Zhipu's CogView-4 as an example, which is an OpenAI-compatible model, you can add it by adding the model configuration in the corresponding AI models file `src/
|
|
35
|
+
Taking Zhipu's CogView-4 as an example, which is an OpenAI-compatible model, you can add it by adding the model configuration in the corresponding AI models file `packages/model-bank/src/aiModels/zhipu.ts`:
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
38
|
const zhipuImageModels: AIImageModelCard[] = [
|
|
@@ -71,7 +71,7 @@ Most Providers use `openaiCompatibleFactory` for OpenAI compatibility. You can p
|
|
|
71
71
|
|
|
72
72
|
1. **Read Provider documentation and standard parameter definitions**
|
|
73
73
|
- Review the Provider's image generation API documentation to understand request and response formats
|
|
74
|
-
- Read `src/
|
|
74
|
+
- Read `packages/model-bank/src/standard-parameters/index.ts` to understand supported parameters
|
|
75
75
|
- Add image model configuration in the corresponding AI models file
|
|
76
76
|
|
|
77
77
|
2. **Implement custom createImage method**
|
|
@@ -87,7 +87,7 @@ Most Providers use `openaiCompatibleFactory` for OpenAI compatibility. You can p
|
|
|
87
87
|
**Code Example**:
|
|
88
88
|
|
|
89
89
|
```ts
|
|
90
|
-
//
|
|
90
|
+
// packages/model-runtime/src/providers/<provider-name>/createImage.ts
|
|
91
91
|
export const createProviderImage = async (
|
|
92
92
|
payload: ImageGenerationPayload,
|
|
93
93
|
options: any,
|
|
@@ -112,7 +112,7 @@ export const createProviderImage = async (
|
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
|
-
//
|
|
115
|
+
// packages/model-runtime/src/providers/<provider-name>/index.ts
|
|
116
116
|
export const LobeProviderAI = openaiCompatibleFactory({
|
|
117
117
|
constructorOptions: {
|
|
118
118
|
// ... other configurations
|
|
@@ -130,7 +130,7 @@ If your Provider has an independent class implementation, you can directly add t
|
|
|
130
130
|
|
|
131
131
|
1. **Read Provider documentation and standard parameter definitions**
|
|
132
132
|
- Review the Provider's image generation API documentation
|
|
133
|
-
- Read `src/
|
|
133
|
+
- Read `packages/model-bank/src/standard-parameters/index.ts`
|
|
134
134
|
- Add image model configuration in the corresponding AI models file
|
|
135
135
|
|
|
136
136
|
2. **Implement createImage method in Provider class**
|
|
@@ -144,7 +144,7 @@ If your Provider has an independent class implementation, you can directly add t
|
|
|
144
144
|
**Code Example**:
|
|
145
145
|
|
|
146
146
|
```ts
|
|
147
|
-
//
|
|
147
|
+
// packages/model-runtime/src/providers/<provider-name>/index.ts
|
|
148
148
|
export class LobeProviderAI {
|
|
149
149
|
async createImage(
|
|
150
150
|
payload: ImageGenerationPayload,
|
|
@@ -13,7 +13,7 @@ tags:
|
|
|
13
13
|
|
|
14
14
|
## 参数标准化
|
|
15
15
|
|
|
16
|
-
所有图像生成模型都必须使用 `src/
|
|
16
|
+
所有图像生成模型都必须使用 `packages/model-bank/src/standard-parameters/index.ts` 中定义的标准参数。这确保了不同 Provider 之间的参数一致性,让用户体验更加统一。
|
|
17
17
|
|
|
18
18
|
**支持的标准参数**:
|
|
19
19
|
|
|
@@ -30,7 +30,7 @@ tags:
|
|
|
30
30
|
|
|
31
31
|
指的是可以使用 openai SDK 进行请求,并且请求参数和和返回值和 dall-e 以及 gpt-image-x 系列一致。
|
|
32
32
|
|
|
33
|
-
以智谱的 CogView-4 为例,它是一个兼容 openai 请求格式的模型。你只需要在对应的 ai models 文件 `src/
|
|
33
|
+
以智谱的 CogView-4 为例,它是一个兼容 openai 请求格式的模型。你只需要在对应的 ai models 文件 `packages/model-bank/src/aiModels/zhipu.ts` 中,添加模型配置,例如:
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
36
|
const zhipuImageModels: AIImageModelCard[] = [
|
|
@@ -69,7 +69,7 @@ const zhipuImageModels: AIImageModelCard[] = [
|
|
|
69
69
|
|
|
70
70
|
1. **阅读 Provider 官方文档和标准参数定义**
|
|
71
71
|
- 查看 Provider 的图像生成 API 文档,了解请求格式和响应格式
|
|
72
|
-
- 阅读 `src/
|
|
72
|
+
- 阅读 `packages/model-bank/src/standard-parameters/index.ts`,了解支持的参数
|
|
73
73
|
- 在对应的 ai models 文件中增加 image model 配置
|
|
74
74
|
|
|
75
75
|
2. **实现自定义的 createImage 方法**
|
|
@@ -85,7 +85,7 @@ const zhipuImageModels: AIImageModelCard[] = [
|
|
|
85
85
|
**代码示例**:
|
|
86
86
|
|
|
87
87
|
```ts
|
|
88
|
-
//
|
|
88
|
+
// packages/model-runtime/src/providers/<provider-name>/createImage.ts
|
|
89
89
|
export const createProviderImage = async (
|
|
90
90
|
payload: ImageGenerationPayload,
|
|
91
91
|
options: any,
|
|
@@ -110,7 +110,7 @@ export const createProviderImage = async (
|
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
```ts
|
|
113
|
-
//
|
|
113
|
+
// packages/model-runtime/src/providers/<provider-name>/index.ts
|
|
114
114
|
export const LobeProviderAI = openaiCompatibleFactory({
|
|
115
115
|
constructorOptions: {
|
|
116
116
|
// ... 其他配置
|
|
@@ -128,7 +128,7 @@ export const LobeProviderAI = openaiCompatibleFactory({
|
|
|
128
128
|
|
|
129
129
|
1. **阅读 Provider 官方文档和标准参数定义**
|
|
130
130
|
- 查看 Provider 的图像生成 API 文档
|
|
131
|
-
- 阅读 `src/
|
|
131
|
+
- 阅读 `packages/model-bank/src/standard-parameters/index.ts`
|
|
132
132
|
- 在对应的 ai models 文件中增加 image model 配置
|
|
133
133
|
|
|
134
134
|
2. **在 Provider 类中实现 createImage 方法**
|
|
@@ -142,7 +142,7 @@ export const LobeProviderAI = openaiCompatibleFactory({
|
|
|
142
142
|
**代码示例**:
|
|
143
143
|
|
|
144
144
|
```ts
|
|
145
|
-
//
|
|
145
|
+
// packages/model-runtime/src/providers/<provider-name>/index.ts
|
|
146
146
|
export class LobeProviderAI {
|
|
147
147
|
async createImage(
|
|
148
148
|
payload: ImageGenerationPayload,
|
|
@@ -1,50 +1,114 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Architecture Design
|
|
3
3
|
description: >-
|
|
4
|
-
Explore the architecture of LobeHub, an AI
|
|
5
|
-
|
|
4
|
+
Explore the architecture of LobeHub, an open-source AI Agent platform
|
|
5
|
+
built on Next.js, covering frontend, backend, runtime, and data storage.
|
|
6
6
|
tags:
|
|
7
7
|
- LobeHub
|
|
8
|
-
- AI Chat Application
|
|
9
|
-
- Next.js
|
|
10
8
|
- Architecture Design
|
|
11
|
-
-
|
|
9
|
+
- Agent Platform
|
|
10
|
+
- Next.js
|
|
12
11
|
---
|
|
13
12
|
|
|
14
13
|
# Architecture Design
|
|
15
14
|
|
|
16
|
-
LobeHub is an AI
|
|
15
|
+
LobeHub is an open-source AI Agent platform built on Next.js, enabling users to interact with AI through natural language, use tools, manage knowledge bases, and more. The following is an overview of LobeHub's architecture design.
|
|
17
16
|
|
|
18
17
|
## Application Architecture Overview
|
|
19
18
|
|
|
20
|
-
The overall architecture of LobeHub consists of the
|
|
19
|
+
The overall architecture of LobeHub consists of the following core layers:
|
|
20
|
+
|
|
21
|
+
```plaintext
|
|
22
|
+
+---------------------+--------------------------------------------------+
|
|
23
|
+
| Layer | Description |
|
|
24
|
+
+---------------------+--------------------------------------------------+
|
|
25
|
+
| Frontend | Next.js RSC + React Router DOM hybrid SPA |
|
|
26
|
+
| Backend API | RESTful WebAPI + tRPC Routers |
|
|
27
|
+
| Runtime | Model Runtime + Agent Runtime |
|
|
28
|
+
| Auth | Better Auth (email/password + SSO) |
|
|
29
|
+
| Data Storage | PostgreSQL + Redis + S3 |
|
|
30
|
+
| Marketplace | Agent Market + MCP Tool Market |
|
|
31
|
+
+---------------------+--------------------------------------------------+
|
|
32
|
+
```
|
|
21
33
|
|
|
22
34
|
## Frontend Architecture
|
|
23
35
|
|
|
24
|
-
The frontend
|
|
36
|
+
The frontend uses the Next.js framework with a **Next.js RSC + React Router DOM hybrid routing** approach: Next.js App Router handles server-rendered pages (e.g., auth pages), while React Router DOM powers the main SPA.
|
|
37
|
+
|
|
38
|
+
Key tech stack:
|
|
39
|
+
|
|
40
|
+
- **UI Components**: `@lobehub/ui`, antd
|
|
41
|
+
- **CSS-in-JS**: antd-style
|
|
42
|
+
- **State Management**: zustand (slice pattern)
|
|
43
|
+
- **Data Fetching**: SWR + tRPC
|
|
44
|
+
- **i18n**: react-i18next
|
|
45
|
+
|
|
46
|
+
Frontend code is organized by responsibility under `src/`. See [Directory Structure](/docs/development/basic/folder-structure) for details.
|
|
47
|
+
|
|
48
|
+
## Backend API
|
|
49
|
+
|
|
50
|
+
The backend provides two API styles:
|
|
51
|
+
|
|
52
|
+
- **RESTful WebAPI** (`src/app/(backend)/webapi/`): Handles endpoints requiring special processing such as chat streaming, TTS, and file serving
|
|
53
|
+
- **tRPC Routers** (`src/server/routers/`): Type-safe main business routes, grouped by runtime:
|
|
54
|
+
- `lambda/` — Main business (agent, session, message, topic, file, knowledge, settings, etc.)
|
|
55
|
+
- `async/` — Long-running async operations (file processing, image generation, RAG evaluation)
|
|
56
|
+
- `tools/` — Tool invocations (search, MCP, market)
|
|
57
|
+
- `mobile/` — Mobile-specific routes
|
|
58
|
+
|
|
59
|
+
## Runtime
|
|
60
|
+
|
|
61
|
+
### Model Runtime
|
|
62
|
+
|
|
63
|
+
`@lobechat/model-runtime` (`packages/model-runtime/`) is the LLM API adapter layer that normalizes API differences across 30+ AI providers (OpenAI, Anthropic, Google, Bedrock, Ollama, etc.), providing a unified calling interface. Each provider has its own adapter implementation. It is stateless — each call is independent.
|
|
64
|
+
|
|
65
|
+
### Agent Runtime
|
|
66
|
+
|
|
67
|
+
`@lobechat/agent-runtime` (`packages/agent-runtime/`) is the agent orchestration engine that sits above Model Runtime, driving the full lifecycle of multi-step AI agent behavior:
|
|
25
68
|
|
|
26
|
-
|
|
69
|
+
- **Plan-Execute Loop**: Core state machine cycling through LLM calls → tool execution → result processing
|
|
70
|
+
- **Tool Invocation & Batch Execution**: Supports single and batch tool calls
|
|
71
|
+
- **Human-in-the-Loop**: Security checks and human approval flows
|
|
72
|
+
- **Context Compression**: Manages context window limits
|
|
73
|
+
- **Usage & Cost Tracking**: Accumulates token usage and monetary costs
|
|
74
|
+
- **Multi-Agent Orchestration**: `GroupOrchestrationRuntime` supports Supervisor + Executor pattern for multi-agent collaboration
|
|
27
75
|
|
|
28
|
-
|
|
76
|
+
In short: Model Runtime handles "how to communicate with an LLM provider"; Agent Runtime handles "how to run a complete agent using LLMs, tools, and human approvals."
|
|
29
77
|
|
|
30
|
-
|
|
78
|
+
## Authentication
|
|
31
79
|
|
|
32
|
-
|
|
80
|
+
LobeHub uses [Better Auth](https://www.better-auth.com/) as the authentication framework, supporting:
|
|
33
81
|
|
|
34
|
-
|
|
82
|
+
- Email + password registration and login
|
|
83
|
+
- SSO single sign-on (GitHub, Google, and various OAuth providers)
|
|
35
84
|
|
|
36
|
-
|
|
85
|
+
Auth configuration is in `src/auth.ts`, with related routes under `src/app/(backend)/api/`.
|
|
37
86
|
|
|
38
|
-
|
|
87
|
+
## Data Storage
|
|
39
88
|
|
|
40
|
-
|
|
89
|
+
```plaintext
|
|
90
|
+
+---------------+----------------------------------------------+
|
|
91
|
+
| Storage | Usage |
|
|
92
|
+
+---------------+----------------------------------------------+
|
|
93
|
+
| PostgreSQL | Primary database for users, sessions, |
|
|
94
|
+
| | messages, agent configs, etc. |
|
|
95
|
+
| Redis | Caching, session state, rate limiting |
|
|
96
|
+
| S3 | File storage (uploads, images, knowledge |
|
|
97
|
+
| | base files, etc.) |
|
|
98
|
+
+---------------+----------------------------------------------+
|
|
99
|
+
```
|
|
41
100
|
|
|
42
|
-
|
|
101
|
+
Database operations use Drizzle ORM, with schemas defined in `packages/database/src/schemas/`.
|
|
43
102
|
|
|
44
|
-
|
|
103
|
+
## Marketplace
|
|
45
104
|
|
|
46
|
-
|
|
105
|
+
- **Agent Market**: Provides AI agents for various scenarios; users can discover, use, and share agents
|
|
106
|
+
- **MCP Tool Market**: Discover and integrate MCP tools to extend agent capabilities
|
|
47
107
|
|
|
48
|
-
|
|
108
|
+
## Development and Deployment
|
|
49
109
|
|
|
50
|
-
|
|
110
|
+
- **Version Control**: Git + GitHub, gitmoji commit conventions
|
|
111
|
+
- **Code Quality**: ESLint, Stylelint, TypeScript type checking, circular dependency detection (dpdm), dead code detection (knip)
|
|
112
|
+
- **Testing**: Vitest unit tests + Cucumber/Playwright E2E tests
|
|
113
|
+
- **CI/CD**: GitHub Actions for automated testing, building, and releasing
|
|
114
|
+
- **Deployment**: Supports Vercel, Docker, and self-hosting on major cloud platforms
|