mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-09 23:36:02 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4011a302af | ||
|
|
deb725a2e2 | ||
|
|
33eb866660 | ||
|
|
34e2fa03ce | ||
|
|
7b63bcdc39 | ||
|
|
d26e81620d | ||
|
|
e7885539a7 | ||
|
|
f216505237 | ||
|
|
8b11eefd0c | ||
|
|
418cddd657 | ||
|
|
75edeb7a01 | ||
|
|
c5aa5be4d8 | ||
|
|
92614062cc | ||
|
|
09307d8c6d | ||
|
|
894db240ae | ||
|
|
f79cde5b0c | ||
|
|
d43c2c498c | ||
|
|
5f6036c5a8 | ||
|
|
dead0794b1 | ||
|
|
f784bad08b | ||
|
|
4e86e1c93d | ||
|
|
c0eec966ac | ||
|
|
62d6dae4f5 | ||
|
|
cab573f3e2 | ||
|
|
8fe59da302 |
11
.github/pull_request_template.md
vendored
11
.github/pull_request_template.md
vendored
@@ -2,17 +2,6 @@
|
|||||||
|
|
||||||
> 请在此部分填写你实现/解决/优化的内容:
|
> 请在此部分填写你实现/解决/优化的内容:
|
||||||
> Summary of what you implemented/solved/optimized:
|
> Summary of what you implemented/solved/optimized:
|
||||||
>
|
|
||||||
|
|
||||||
### 更改前后对比截图 / Screenshots
|
|
||||||
|
|
||||||
> 请在此部分粘贴更改前后对比截图(可以是界面截图、控制台输出、对话截图等):
|
|
||||||
> Please paste the screenshots of changes before and after here (can be interface screenshots, console output, conversation screenshots, etc.):
|
|
||||||
>
|
|
||||||
> 修改前 / Before:
|
|
||||||
>
|
|
||||||
> 修改后 / After:
|
|
||||||
>
|
|
||||||
|
|
||||||
## 检查清单 / Checklist
|
## 检查清单 / Checklist
|
||||||
|
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -31,16 +31,25 @@ LangBot 是一个开源的大语言模型原生即时通信机器人开发平台
|
|||||||
|
|
||||||
## 📦 开始使用
|
## 📦 开始使用
|
||||||
|
|
||||||
#### 快速部署
|
#### 快速体验(推荐)
|
||||||
|
|
||||||
使用 `uvx` 一键启动(需要先安装 [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
使用 `uvx` 一键启动(无需安装):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx langbot
|
uvx langbot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
或使用 `pip` 安装后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install langbot
|
||||||
|
langbot
|
||||||
|
```
|
||||||
|
|
||||||
访问 http://localhost:5300 即可开始使用。
|
访问 http://localhost:5300 即可开始使用。
|
||||||
|
|
||||||
|
详细文档[PyPI 安装](docs/PYPI_INSTALLATION.md)。
|
||||||
|
|
||||||
#### Docker Compose 部署
|
#### Docker Compose 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
13
README_EN.md
13
README_EN.md
@@ -25,16 +25,25 @@ LangBot is an open-source LLM native instant messaging robot development platfor
|
|||||||
|
|
||||||
## 📦 Getting Started
|
## 📦 Getting Started
|
||||||
|
|
||||||
#### Quick Start
|
#### Quick Start (Recommended)
|
||||||
|
|
||||||
Use `uvx` to start with one command (need to install [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
Use `uvx` to start with one command (no installation required):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx langbot
|
uvx langbot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or install with `pip` and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install langbot
|
||||||
|
langbot
|
||||||
|
```
|
||||||
|
|
||||||
Visit http://localhost:5300 to start using it.
|
Visit http://localhost:5300 to start using it.
|
||||||
|
|
||||||
|
Detailed documentation [PyPI Installation](docs/PYPI_INSTALLATION.md).
|
||||||
|
|
||||||
#### Docker Compose Deployment
|
#### Docker Compose Deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
10
README_JP.md
10
README_JP.md
@@ -25,16 +25,6 @@ LangBot は、エージェント、RAG、MCP などの LLM アプリケーショ
|
|||||||
|
|
||||||
## 📦 始め方
|
## 📦 始め方
|
||||||
|
|
||||||
#### クイックスタート
|
|
||||||
|
|
||||||
`uvx` を使用した迅速なデプロイ([uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uvx langbot
|
|
||||||
```
|
|
||||||
|
|
||||||
http://localhost:5300 にアクセスして使用を開始します。
|
|
||||||
|
|
||||||
#### Docker Compose デプロイ
|
#### Docker Compose デプロイ
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
10
README_TW.md
10
README_TW.md
@@ -27,16 +27,6 @@ LangBot 是一個開源的大語言模型原生即時通訊機器人開發平台
|
|||||||
|
|
||||||
## 📦 開始使用
|
## 📦 開始使用
|
||||||
|
|
||||||
#### 快速部署
|
|
||||||
|
|
||||||
使用 `uvx` 一鍵啟動(需要先安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/) ):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uvx langbot
|
|
||||||
```
|
|
||||||
|
|
||||||
訪問 http://localhost:5300 即可開始使用。
|
|
||||||
|
|
||||||
#### Docker Compose 部署
|
#### Docker Compose 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ services:
|
|||||||
langbot_plugin_runtime:
|
langbot_plugin_runtime:
|
||||||
image: rockchin/langbot:latest
|
image: rockchin/langbot:latest
|
||||||
container_name: langbot_plugin_runtime
|
container_name: langbot_plugin_runtime
|
||||||
platform: linux/amd64 # For Apple Silicon compatibility
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/plugins:/app/data/plugins
|
- ./data/plugins:/app/data/plugins
|
||||||
ports:
|
ports:
|
||||||
@@ -22,7 +21,6 @@ services:
|
|||||||
langbot:
|
langbot:
|
||||||
image: rockchin/langbot:latest
|
image: rockchin/langbot:latest
|
||||||
container_name: langbot
|
container_name: langbot
|
||||||
platform: linux/amd64 # For Apple Silicon compatibility
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./plugins:/app/plugins
|
- ./plugins:/app/plugins
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.5.3"
|
version = "4.6.0-beta.2"
|
||||||
description = "Easy-to-use global IM bot platform designed for LLM era"
|
description = "Easy-to-use global IM bot platform designed for LLM era"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
@@ -63,7 +63,7 @@ dependencies = [
|
|||||||
"langchain-text-splitters>=0.0.1",
|
"langchain-text-splitters>=0.0.1",
|
||||||
"chromadb>=0.4.24",
|
"chromadb>=0.4.24",
|
||||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||||
"langbot-plugin==0.1.11",
|
"langbot-plugin==0.1.11b1",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"line-bot-sdk>=3.19.0",
|
"line-bot-sdk>=3.19.0",
|
||||||
"tboxsdk>=0.0.10",
|
"tboxsdk>=0.0.10",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
|
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
|
||||||
|
|
||||||
__version__ = '4.5.3'
|
__version__ = '4.6.0-beta.2'
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import langbot
|
semantic_version = 'v4.6.0-beta.2'
|
||||||
|
|
||||||
semantic_version = f'v{langbot.__version__}'
|
|
||||||
|
|
||||||
required_database_version = 11
|
required_database_version = 11
|
||||||
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
|
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
|
||||||
|
|||||||
@@ -188,6 +188,40 @@ export default function HomeSidebar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SidebarChild
|
||||||
|
onClick={() => {
|
||||||
|
// open docs.langbot.app
|
||||||
|
const language = localStorage.getItem('langbot_language');
|
||||||
|
if (language === 'zh-Hans') {
|
||||||
|
window.open(
|
||||||
|
'https://docs.langbot.app/zh/insight/guide.html',
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
} else if (language === 'zh-Hant') {
|
||||||
|
window.open(
|
||||||
|
'https://docs.langbot.app/zh/insight/guide.html',
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window.open(
|
||||||
|
'https://docs.langbot.app/en/insight/guide.html',
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isSelected={false}
|
||||||
|
icon={
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM13 13.3551V14H11V12.5C11 11.9477 11.4477 11.5 12 11.5C12.8284 11.5 13.5 10.8284 13.5 10C13.5 9.17157 12.8284 8.5 12 8.5C11.2723 8.5 10.6656 9.01823 10.5288 9.70577L8.56731 9.31346C8.88637 7.70919 10.302 6.5 12 6.5C13.933 6.5 15.5 8.067 15.5 10C15.5 11.5855 14.4457 12.9248 13 13.3551Z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
name={t('common.helpDocs')}
|
||||||
|
/>
|
||||||
|
|
||||||
<SidebarChild
|
<SidebarChild
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setApiKeyDialogOpen(true);
|
setApiKeyDialogOpen(true);
|
||||||
@@ -268,41 +302,6 @@ export default function HomeSidebar({
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<span className="text-sm font-medium">{t('common.account')}</span>
|
<span className="text-sm font-medium">{t('common.account')}</span>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full justify-start font-normal"
|
|
||||||
onClick={() => {
|
|
||||||
// open docs.langbot.app
|
|
||||||
const language = localStorage.getItem('langbot_language');
|
|
||||||
if (language === 'zh-Hans') {
|
|
||||||
window.open(
|
|
||||||
'https://docs.langbot.app/zh/insight/guide.html',
|
|
||||||
'_blank',
|
|
||||||
);
|
|
||||||
} else if (language === 'zh-Hant') {
|
|
||||||
window.open(
|
|
||||||
'https://docs.langbot.app/zh/insight/guide.html',
|
|
||||||
'_blank',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
window.open(
|
|
||||||
'https://docs.langbot.app/en/insight/guide.html',
|
|
||||||
'_blank',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setPopoverOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
className="w-4 h-4 mr-2"
|
|
||||||
>
|
|
||||||
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM13 13.3551V14H11V12.5C11 11.9477 11.4477 11.5 12 11.5C12.8284 11.5 13.5 10.8284 13.5 10C13.5 9.17157 12.8284 8.5 12 8.5C11.2723 8.5 10.6656 9.01823 10.5288 9.70577L8.56731 9.31346C8.88637 7.70919 10.302 6.5 12 6.5C13.933 6.5 15.5 8.067 15.5 10C15.5 11.5855 14.4457 12.9248 13 13.3551Z"></path>
|
|
||||||
</svg>
|
|
||||||
{t('common.helpDocs')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full justify-start font-normal"
|
className="w-full justify-start font-normal"
|
||||||
|
|||||||
@@ -331,56 +331,48 @@ export default function PipelineExtension({
|
|||||||
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
||||||
{allPlugins.length === 0 ? (
|
{allPlugins.map((plugin) => {
|
||||||
<div className="flex h-full items-center justify-center">
|
const pluginId = getPluginId(plugin);
|
||||||
<p className="text-sm text-muted-foreground">
|
const metadata = plugin.manifest.manifest.metadata;
|
||||||
{t('pipelines.extensions.noPluginsInstalled')}
|
const isSelected = tempSelectedPluginIds.includes(pluginId);
|
||||||
</p>
|
return (
|
||||||
</div>
|
<div
|
||||||
) : (
|
key={pluginId}
|
||||||
allPlugins.map((plugin) => {
|
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
||||||
const pluginId = getPluginId(plugin);
|
onClick={() => handleTogglePlugin(pluginId)}
|
||||||
const metadata = plugin.manifest.manifest.metadata;
|
>
|
||||||
const isSelected = tempSelectedPluginIds.includes(pluginId);
|
<Checkbox checked={isSelected} />
|
||||||
return (
|
<img
|
||||||
<div
|
src={backendClient.getPluginIconURL(
|
||||||
key={pluginId}
|
metadata.author || '',
|
||||||
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
metadata.name,
|
||||||
onClick={() => handleTogglePlugin(pluginId)}
|
|
||||||
>
|
|
||||||
<Checkbox checked={isSelected} />
|
|
||||||
<img
|
|
||||||
src={backendClient.getPluginIconURL(
|
|
||||||
metadata.author || '',
|
|
||||||
metadata.name,
|
|
||||||
)}
|
|
||||||
alt={metadata.name}
|
|
||||||
className="w-10 h-10 rounded-lg border bg-muted object-cover flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-medium">{metadata.name}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{metadata.author} • v{metadata.version}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1 mt-1">
|
|
||||||
<PluginComponentList
|
|
||||||
components={plugin.components}
|
|
||||||
showComponentName={true}
|
|
||||||
showTitle={false}
|
|
||||||
useBadge={true}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!plugin.enabled && (
|
|
||||||
<Badge variant="secondary">
|
|
||||||
{t('pipelines.extensions.disabled')}
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
|
alt={metadata.name}
|
||||||
|
className="w-10 h-10 rounded-lg border bg-muted object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">{metadata.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{metadata.author} • v{metadata.version}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 mt-1">
|
||||||
|
<PluginComponentList
|
||||||
|
components={plugin.components}
|
||||||
|
showComponentName={true}
|
||||||
|
showTitle={false}
|
||||||
|
useBadge={true}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
{!plugin.enabled && (
|
||||||
})
|
<Badge variant="secondary">
|
||||||
)}
|
{t('pipelines.extensions.disabled')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
@@ -405,56 +397,46 @@ export default function PipelineExtension({
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
||||||
{allMCPServers.length === 0 ? (
|
{allMCPServers.map((server) => {
|
||||||
<div className="flex h-full items-center justify-center">
|
const isSelected = tempSelectedMCPIds.includes(server.uuid || '');
|
||||||
<p className="text-sm text-muted-foreground">
|
return (
|
||||||
{t('pipelines.extensions.noMCPServersConfigured')}
|
<div
|
||||||
</p>
|
key={server.uuid}
|
||||||
</div>
|
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
||||||
) : (
|
onClick={() => handleToggleMCPServer(server.uuid || '')}
|
||||||
allMCPServers.map((server) => {
|
>
|
||||||
const isSelected = tempSelectedMCPIds.includes(
|
<Checkbox checked={isSelected} />
|
||||||
server.uuid || '',
|
<div className="w-10 h-10 rounded-lg border bg-muted flex items-center justify-center flex-shrink-0">
|
||||||
);
|
<Server className="h-5 w-5 text-muted-foreground" />
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={server.uuid}
|
|
||||||
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
|
||||||
onClick={() => handleToggleMCPServer(server.uuid || '')}
|
|
||||||
>
|
|
||||||
<Checkbox checked={isSelected} />
|
|
||||||
<div className="w-10 h-10 rounded-lg border bg-muted flex items-center justify-center flex-shrink-0">
|
|
||||||
<Server className="h-5 w-5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-medium">{server.name}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{server.mode}
|
|
||||||
</div>
|
|
||||||
{server.runtime_info &&
|
|
||||||
server.runtime_info.status === 'connected' && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="flex items-center gap-1 mt-1"
|
|
||||||
>
|
|
||||||
<Wrench className="h-3 w-3 text-black dark:text-white" />
|
|
||||||
<span className="text-xs text-black dark:text-white">
|
|
||||||
{t('pipelines.extensions.toolCount', {
|
|
||||||
count: server.runtime_info.tool_count || 0,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!server.enable && (
|
|
||||||
<Badge variant="secondary">
|
|
||||||
{t('pipelines.extensions.disabled')}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="flex-1">
|
||||||
})
|
<div className="font-medium">{server.name}</div>
|
||||||
)}
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{server.mode}
|
||||||
|
</div>
|
||||||
|
{server.runtime_info &&
|
||||||
|
server.runtime_info.status === 'connected' && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-1 mt-1"
|
||||||
|
>
|
||||||
|
<Wrench className="h-3 w-3 text-black dark:text-white" />
|
||||||
|
<span className="text-xs text-black dark:text-white">
|
||||||
|
{t('pipelines.extensions.toolCount', {
|
||||||
|
count: server.runtime_info.tool_count || 0,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!server.enable && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{t('pipelines.extensions.disabled')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setMcpDialogOpen(false)}>
|
<Button variant="outline" onClick={() => setMcpDialogOpen(false)}>
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{pluginList.length === 0 ? (
|
{pluginList.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="h-[3rem] w-[3rem]"
|
className="h-[3rem] w-[3rem]"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ function MarketPageContent({
|
|||||||
|
|
||||||
const pageSize = 16; // 每页16个,4行x4列
|
const pageSize = 16; // 每页16个,4行x4列
|
||||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
// 排序选项
|
// 排序选项
|
||||||
const sortOptions: SortOption[] = [
|
const sortOptions: SortOption[] = [
|
||||||
@@ -263,21 +262,19 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
}, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]);
|
}, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]);
|
||||||
|
|
||||||
// Listen to scroll events on the scroll container
|
// 监听滚动事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollContainer = scrollContainerRef.current;
|
|
||||||
if (!scrollContainer) return;
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
if (
|
||||||
// Load more when scrolled to within 100px of the bottom
|
window.innerHeight + document.documentElement.scrollTop >=
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
document.documentElement.offsetHeight - 100
|
||||||
|
) {
|
||||||
loadMore();
|
loadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
scrollContainer.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
}, [loadMore]);
|
}, [loadMore]);
|
||||||
|
|
||||||
// 安装插件
|
// 安装插件
|
||||||
@@ -286,109 +283,99 @@ function MarketPageContent({
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="container mx-auto px-3 sm:px-4 py-4 sm:py-6 space-y-4 sm:space-y-6">
|
||||||
{/* Fixed header with search and sort controls */}
|
{/* 搜索框 */}
|
||||||
<div className="flex-shrink-0 space-y-4 px-3 sm:px-4 py-4 sm:py-6">
|
<div className="flex items-center justify-center">
|
||||||
{/* Search box */}
|
<div className="relative w-full max-w-2xl">
|
||||||
<div className="flex items-center justify-center">
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||||
<div className="relative w-full max-w-2xl">
|
<Input
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
placeholder={t('market.searchPlaceholder')}
|
||||||
<Input
|
value={searchQuery}
|
||||||
placeholder={t('market.searchPlaceholder')}
|
onChange={(e) => handleSearchInputChange(e.target.value)}
|
||||||
value={searchQuery}
|
onKeyPress={(e) => {
|
||||||
onChange={(e) => handleSearchInputChange(e.target.value)}
|
if (e.key === 'Enter') {
|
||||||
onKeyPress={(e) => {
|
// 立即搜索,清除防抖定时器
|
||||||
if (e.key === 'Enter') {
|
if (searchTimeoutRef.current) {
|
||||||
// Immediately search, clear debounce timer
|
clearTimeout(searchTimeoutRef.current);
|
||||||
if (searchTimeoutRef.current) {
|
|
||||||
clearTimeout(searchTimeoutRef.current);
|
|
||||||
}
|
|
||||||
handleSearch(searchQuery);
|
|
||||||
}
|
}
|
||||||
}}
|
handleSearch(searchQuery);
|
||||||
className="pl-10 pr-4 text-sm sm:text-base"
|
}
|
||||||
/>
|
}}
|
||||||
</div>
|
className="pl-10 pr-4 text-sm sm:text-base"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sort dropdown */}
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
|
|
||||||
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
|
||||||
{t('market.sortBy')}:
|
|
||||||
</span>
|
|
||||||
<Select value={sortOption} onValueChange={handleSortChange}>
|
|
||||||
<SelectTrigger className="w-40 sm:w-48 text-xs sm:text-sm">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{sortOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search results stats */}
|
|
||||||
{total > 0 && (
|
|
||||||
<div className="text-center text-muted-foreground text-sm">
|
|
||||||
{searchQuery
|
|
||||||
? t('market.searchResults', { count: total })
|
|
||||||
: t('market.totalPlugins', { count: total })}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable content area */}
|
{/* 排序下拉框 */}
|
||||||
<div
|
<div className="flex items-center justify-center">
|
||||||
ref={scrollContainerRef}
|
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
|
||||||
className="flex-1 overflow-y-auto px-3 sm:px-4"
|
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
||||||
>
|
{t('market.sortBy')}:
|
||||||
{isLoading ? (
|
</span>
|
||||||
<div className="flex items-center justify-center py-12">
|
<Select value={sortOption} onValueChange={handleSortChange}>
|
||||||
<Loader2 className="h-8 w-8 animate-spin" />
|
<SelectTrigger className="w-40 sm:w-48 text-xs sm:text-sm">
|
||||||
<span className="ml-2">{t('market.loading')}</span>
|
<SelectValue />
|
||||||
</div>
|
</SelectTrigger>
|
||||||
) : plugins.length === 0 ? (
|
<SelectContent>
|
||||||
<div className="flex items-center justify-center py-12">
|
{sortOptions.map((option) => (
|
||||||
<div className="text-muted-foreground">
|
<SelectItem key={option.value} value={option.value}>
|
||||||
{searchQuery ? t('market.noResults') : t('market.noPlugins')}
|
{option.label}
|
||||||
</div>
|
</SelectItem>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6 pb-6">
|
|
||||||
{plugins.map((plugin) => (
|
|
||||||
<PluginMarketCardComponent
|
|
||||||
key={plugin.pluginId}
|
|
||||||
cardVO={plugin}
|
|
||||||
onPluginClick={handlePluginClick}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
{/* Loading more indicator */}
|
</div>
|
||||||
{isLoadingMore && (
|
|
||||||
<div className="flex items-center justify-center py-6">
|
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
|
||||||
<span className="ml-2">{t('market.loadingMore')}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* No more data hint */}
|
|
||||||
{!hasMore && plugins.length > 0 && (
|
|
||||||
<div className="text-center text-muted-foreground py-6">
|
|
||||||
{t('market.allLoaded')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plugin detail dialog */}
|
{/* 搜索结果统计 */}
|
||||||
|
{total > 0 && (
|
||||||
|
<div className="text-center text-muted-foreground text-sm">
|
||||||
|
{searchQuery
|
||||||
|
? t('market.searchResults', { count: total })
|
||||||
|
: t('market.totalPlugins', { count: total })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 插件列表 */}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin" />
|
||||||
|
<span className="ml-2">{t('market.loading')}</span>
|
||||||
|
</div>
|
||||||
|
) : plugins.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{searchQuery ? t('market.noResults') : t('market.noPlugins')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6">
|
||||||
|
{plugins.map((plugin) => (
|
||||||
|
<PluginMarketCardComponent
|
||||||
|
key={plugin.pluginId}
|
||||||
|
cardVO={plugin}
|
||||||
|
onPluginClick={handlePluginClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 加载更多指示器 */}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="flex items-center justify-center py-6">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
<span className="ml-2">{t('market.loadingMore')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 没有更多数据提示 */}
|
||||||
|
{!hasMore && plugins.length > 0 && (
|
||||||
|
<div className="text-center text-muted-foreground py-6">
|
||||||
|
{t('market.allLoaded')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 插件详情对话框 */}
|
||||||
<PluginDetailDialog
|
<PluginDetailDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onOpenChange={handleDialogClose}
|
onOpenChange={handleDialogClose}
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ export default function MCPComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
{/* Server list */}
|
{/* 已安装的服务器列表 */}
|
||||||
<div className="w-full h-full px-[0.8rem] pt-[0rem]">
|
<div className="w-full px-[0.8rem] pt-[0rem] gap-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
||||||
{t('mcp.loading')}
|
{t('mcp.loading')}
|
||||||
</div>
|
</div>
|
||||||
) : installedServers.length === 0 ? (
|
) : installedServers.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="h-[3rem] w-[3rem]"
|
className="h-[3rem] w-[3rem]"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -92,7 +92,7 @@ export default function MCPComponent({
|
|||||||
<div className="text-lg mb-2">{t('mcp.noServerInstalled')}</div>
|
<div className="text-lg mb-2">{t('mcp.noServerInstalled')}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pt-[2rem] pb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pt-[2rem]">
|
||||||
{installedServers.map((server, index) => (
|
{installedServers.map((server, index) => (
|
||||||
<div key={`${server.name}-${index}`}>
|
<div key={`${server.name}-${index}`}>
|
||||||
<MCPCardComponent
|
<MCPCardComponent
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ export default function PluginConfigPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.pageContainer} h-full flex flex-col ${isDragOver ? 'bg-blue-50' : ''}`}
|
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -443,12 +443,8 @@ export default function PluginConfigPage() {
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
<Tabs
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
value={activeTab}
|
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
||||||
onValueChange={setActiveTab}
|
|
||||||
className="w-full h-full flex flex-col"
|
|
||||||
>
|
|
||||||
<div className="flex flex-row justify-between items-center px-[0.8rem] flex-shrink-0">
|
|
||||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
||||||
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
||||||
{t('plugins.installed')}
|
{t('plugins.installed')}
|
||||||
@@ -526,10 +522,10 @@ export default function PluginConfigPage() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TabsContent value="installed" className="flex-1 overflow-y-auto mt-0">
|
<TabsContent value="installed">
|
||||||
<PluginInstalledComponent ref={pluginInstalledRef} />
|
<PluginInstalledComponent ref={pluginInstalledRef} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="market" className="flex-1 overflow-y-auto mt-0">
|
<TabsContent value="market">
|
||||||
<MarketPage
|
<MarketPage
|
||||||
installPlugin={(plugin: PluginV4) => {
|
installPlugin={(plugin: PluginV4) => {
|
||||||
setInstallSource('marketplace');
|
setInstallSource('marketplace');
|
||||||
@@ -543,10 +539,7 @@ export default function PluginConfigPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent value="mcp-servers">
|
||||||
value="mcp-servers"
|
|
||||||
className="flex-1 overflow-y-auto mt-0"
|
|
||||||
>
|
|
||||||
<MCPServerComponent
|
<MCPServerComponent
|
||||||
key={refreshKey}
|
key={refreshKey}
|
||||||
onEditServer={(serverName) => {
|
onEditServer={(serverName) => {
|
||||||
|
|||||||
@@ -482,8 +482,6 @@ const enUS = {
|
|||||||
addMCPServer: 'Add MCP Server',
|
addMCPServer: 'Add MCP Server',
|
||||||
selectMCPServers: 'Select MCP Servers',
|
selectMCPServers: 'Select MCP Servers',
|
||||||
toolCount: '{{count}} tools',
|
toolCount: '{{count}} tools',
|
||||||
noPluginsInstalled: 'No installed plugins',
|
|
||||||
noMCPServersConfigured: 'No configured MCP servers',
|
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'Pipeline Chat',
|
title: 'Pipeline Chat',
|
||||||
|
|||||||
@@ -485,8 +485,6 @@ const jaJP = {
|
|||||||
addMCPServer: 'MCPサーバーを追加',
|
addMCPServer: 'MCPサーバーを追加',
|
||||||
selectMCPServers: 'MCPサーバーを選択',
|
selectMCPServers: 'MCPサーバーを選択',
|
||||||
toolCount: '{{count}}個のツール',
|
toolCount: '{{count}}個のツール',
|
||||||
noPluginsInstalled: 'インストールされているプラグインがありません',
|
|
||||||
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'パイプラインのチャット',
|
title: 'パイプラインのチャット',
|
||||||
|
|||||||
@@ -464,8 +464,6 @@ const zhHans = {
|
|||||||
addMCPServer: '添加 MCP 服务器',
|
addMCPServer: '添加 MCP 服务器',
|
||||||
selectMCPServers: '选择 MCP 服务器',
|
selectMCPServers: '选择 MCP 服务器',
|
||||||
toolCount: '{{count}} 个工具',
|
toolCount: '{{count}} 个工具',
|
||||||
noPluginsInstalled: '无已安装的插件',
|
|
||||||
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流水线对话',
|
title: '流水线对话',
|
||||||
|
|||||||
@@ -462,8 +462,6 @@ const zhHant = {
|
|||||||
addMCPServer: '新增 MCP 伺服器',
|
addMCPServer: '新增 MCP 伺服器',
|
||||||
selectMCPServers: '選擇 MCP 伺服器',
|
selectMCPServers: '選擇 MCP 伺服器',
|
||||||
toolCount: '{{count}} 個工具',
|
toolCount: '{{count}} 個工具',
|
||||||
noPluginsInstalled: '無已安裝的插件',
|
|
||||||
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流程線對話',
|
title: '流程線對話',
|
||||||
|
|||||||
Reference in New Issue
Block a user