mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 19:37:36 +08:00
Compare commits
10 Commits
copilot/ad
...
763c1a885c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
763c1a885c | ||
|
|
dbc09f46f4 | ||
|
|
cf43f09aff | ||
|
|
c3c51b0fbf | ||
|
|
8a42daa63f | ||
|
|
d91d98c9d4 | ||
|
|
2e82f2b2d1 | ||
|
|
f459c7017a | ||
|
|
c27ccb8475 | ||
|
|
abb2f7ae05 |
11
.github/pull_request_template.md
vendored
11
.github/pull_request_template.md
vendored
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
> 请在此部分填写你实现/解决/优化的内容:
|
> 请在此部分填写你实现/解决/优化的内容:
|
||||||
> 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.5.2"
|
version = "4.5.3"
|
||||||
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"]
|
||||||
|
|||||||
@@ -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.2'
|
__version__ = '4.5.3'
|
||||||
|
|||||||
@@ -109,14 +109,13 @@ class WecomClient:
|
|||||||
async def send_image(self, user_id: str, agent_id: int, media_id: str):
|
async def send_image(self, user_id: str, agent_id: int, media_id: str):
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
url = self.base_url + '/media/upload?access_token=' + self.access_token
|
|
||||||
|
url = self.base_url + '/message/send?access_token=' + self.access_token
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
params = {
|
params = {
|
||||||
'touser': user_id,
|
'touser': user_id,
|
||||||
'toparty': '',
|
|
||||||
'totag': '',
|
|
||||||
'agentid': agent_id,
|
|
||||||
'msgtype': 'image',
|
'msgtype': 'image',
|
||||||
|
'agentid': agent_id,
|
||||||
'image': {
|
'image': {
|
||||||
'media_id': media_id,
|
'media_id': media_id,
|
||||||
},
|
},
|
||||||
@@ -125,19 +124,13 @@ class WecomClient:
|
|||||||
'enable_duplicate_check': 0,
|
'enable_duplicate_check': 0,
|
||||||
'duplicate_check_interval': 1800,
|
'duplicate_check_interval': 1800,
|
||||||
}
|
}
|
||||||
try:
|
response = await client.post(url, json=params)
|
||||||
response = await client.post(url, json=params)
|
data = response.json()
|
||||||
data = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(f'发送图片失败:{data}')
|
|
||||||
raise Exception('Failed to send image: ' + str(e))
|
|
||||||
|
|
||||||
# 企业微信错误码40014和42001,代表accesstoken问题
|
|
||||||
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_image(user_id, agent_id, media_id)
|
return await self.send_image(user_id, agent_id, media_id)
|
||||||
|
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f'发送图片失败:{data}')
|
||||||
raise Exception('Failed to send image: ' + str(data))
|
raise Exception('Failed to send image: ' + str(data))
|
||||||
|
|
||||||
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
|
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
semantic_version = 'v4.5.2'
|
import langbot
|
||||||
|
|
||||||
|
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"""
|
||||||
|
|||||||
@@ -289,12 +289,16 @@ export default function ApiIntegrationDialog({
|
|||||||
{t('common.noApiKeys')}
|
{t('common.noApiKeys')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border rounded-md">
|
<div className="border rounded-md overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>{t('common.name')}</TableHead>
|
<TableHead className="min-w-[120px]">
|
||||||
<TableHead>{t('common.apiKeyValue')}</TableHead>
|
{t('common.name')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="min-w-[200px]">
|
||||||
|
{t('common.apiKeyValue')}
|
||||||
|
</TableHead>
|
||||||
<TableHead className="w-[100px]">
|
<TableHead className="w-[100px]">
|
||||||
{t('common.actions')}
|
{t('common.actions')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -372,16 +376,20 @@ export default function ApiIntegrationDialog({
|
|||||||
{t('common.noWebhooks')}
|
{t('common.noWebhooks')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border rounded-md">
|
<div className="border rounded-md overflow-x-auto max-w-full">
|
||||||
<Table>
|
<Table className="table-fixed w-full">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>{t('common.name')}</TableHead>
|
<TableHead className="w-[150px]">
|
||||||
<TableHead>{t('common.webhookUrl')}</TableHead>
|
{t('common.name')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="w-[380px]">
|
||||||
|
{t('common.webhookUrl')}
|
||||||
|
</TableHead>
|
||||||
<TableHead className="w-[80px]">
|
<TableHead className="w-[80px]">
|
||||||
{t('common.webhookEnabled')}
|
{t('common.webhookEnabled')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[100px]">
|
<TableHead className="w-[80px]">
|
||||||
{t('common.actions')}
|
{t('common.actions')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -389,20 +397,30 @@ export default function ApiIntegrationDialog({
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{webhooks.map((webhook) => (
|
{webhooks.map((webhook) => (
|
||||||
<TableRow key={webhook.id}>
|
<TableRow key={webhook.id}>
|
||||||
<TableCell>
|
<TableCell className="truncate">
|
||||||
<div>
|
<div className="truncate">
|
||||||
<div className="font-medium">{webhook.name}</div>
|
<div
|
||||||
|
className="font-medium truncate"
|
||||||
|
title={webhook.name}
|
||||||
|
>
|
||||||
|
{webhook.name}
|
||||||
|
</div>
|
||||||
{webhook.description && (
|
{webhook.description && (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div
|
||||||
|
className="text-sm text-muted-foreground truncate"
|
||||||
|
title={webhook.description}
|
||||||
|
>
|
||||||
{webhook.description}
|
{webhook.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<code className="text-sm bg-muted px-2 py-1 rounded break-all">
|
<div className="overflow-x-auto max-w-[380px]">
|
||||||
{webhook.url}
|
<code className="text-sm bg-muted px-2 py-1 rounded whitespace-nowrap inline-block">
|
||||||
</code>
|
{webhook.url}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export default function DynamicFormItemComponent({
|
|||||||
model.requester,
|
model.requester,
|
||||||
)}
|
)}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
className="w-8 h-8 rounded-full"
|
className="w-8 h-8 rounded-[8%]"
|
||||||
/>
|
/>
|
||||||
<h4 className="font-medium">{model.name}</h4>
|
<h4 className="font-medium">{model.name}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -188,40 +188,6 @@ 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);
|
||||||
@@ -302,6 +268,41 @@ 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"
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export default function KBForm({
|
|||||||
model.requester,
|
model.requester,
|
||||||
)}
|
)}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
className="w-8 h-8 rounded-full"
|
className="w-8 h-8 rounded-[8%]"
|
||||||
/>
|
/>
|
||||||
<h4 className="font-medium">
|
<h4 className="font-medium">
|
||||||
{model.name}
|
{model.name}
|
||||||
|
|||||||
@@ -146,6 +146,26 @@ export default function PipelineExtension({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleAllPlugins = () => {
|
||||||
|
if (tempSelectedPluginIds.length === allPlugins.length) {
|
||||||
|
// Deselect all
|
||||||
|
setTempSelectedPluginIds([]);
|
||||||
|
} else {
|
||||||
|
// Select all
|
||||||
|
setTempSelectedPluginIds(allPlugins.map((p) => getPluginId(p)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAllMCPServers = () => {
|
||||||
|
if (tempSelectedMCPIds.length === allMCPServers.length) {
|
||||||
|
// Deselect all
|
||||||
|
setTempSelectedMCPIds([]);
|
||||||
|
} else {
|
||||||
|
// Select all
|
||||||
|
setTempSelectedMCPIds(allMCPServers.map((s) => s.uuid || ''));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleConfirmPluginSelection = async () => {
|
const handleConfirmPluginSelection = async () => {
|
||||||
const newSelected = allPlugins.filter((p) =>
|
const newSelected = allPlugins.filter((p) =>
|
||||||
tempSelectedPluginIds.includes(getPluginId(p)),
|
tempSelectedPluginIds.includes(getPluginId(p)),
|
||||||
@@ -330,49 +350,74 @@ export default function PipelineExtension({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{allPlugins.length > 0 && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 px-1 py-2 border-b cursor-pointer"
|
||||||
|
onClick={handleToggleAllPlugins}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
tempSelectedPluginIds.length === allPlugins.length &&
|
||||||
|
allPlugins.length > 0
|
||||||
|
}
|
||||||
|
onCheckedChange={handleToggleAllPlugins}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t('pipelines.extensions.selectAll')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<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.map((plugin) => {
|
{allPlugins.length === 0 ? (
|
||||||
const pluginId = getPluginId(plugin);
|
<div className="flex h-full items-center justify-center">
|
||||||
const metadata = plugin.manifest.manifest.metadata;
|
<p className="text-sm text-muted-foreground">
|
||||||
const isSelected = tempSelectedPluginIds.includes(pluginId);
|
{t('pipelines.extensions.noPluginsInstalled')}
|
||||||
return (
|
</p>
|
||||||
<div
|
</div>
|
||||||
key={pluginId}
|
) : (
|
||||||
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
allPlugins.map((plugin) => {
|
||||||
onClick={() => handleTogglePlugin(pluginId)}
|
const pluginId = getPluginId(plugin);
|
||||||
>
|
const metadata = plugin.manifest.manifest.metadata;
|
||||||
<Checkbox checked={isSelected} />
|
const isSelected = tempSelectedPluginIds.includes(pluginId);
|
||||||
<img
|
return (
|
||||||
src={backendClient.getPluginIconURL(
|
<div
|
||||||
metadata.author || '',
|
key={pluginId}
|
||||||
metadata.name,
|
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
||||||
|
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
|
||||||
@@ -396,47 +441,74 @@ export default function PipelineExtension({
|
|||||||
{t('pipelines.extensions.selectMCPServers')}
|
{t('pipelines.extensions.selectMCPServers')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{allMCPServers.length > 0 && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 px-1 py-2 border-b cursor-pointer"
|
||||||
|
onClick={handleToggleAllMCPServers}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
tempSelectedMCPIds.length === allMCPServers.length &&
|
||||||
|
allMCPServers.length > 0
|
||||||
|
}
|
||||||
|
onCheckedChange={handleToggleAllMCPServers}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t('pipelines.extensions.selectAll')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<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.map((server) => {
|
{allMCPServers.length === 0 ? (
|
||||||
const isSelected = tempSelectedMCPIds.includes(server.uuid || '');
|
<div className="flex h-full items-center justify-center">
|
||||||
return (
|
<p className="text-sm text-muted-foreground">
|
||||||
<div
|
{t('pipelines.extensions.noMCPServersConfigured')}
|
||||||
key={server.uuid}
|
</p>
|
||||||
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
</div>
|
||||||
onClick={() => handleToggleMCPServer(server.uuid || '')}
|
) : (
|
||||||
>
|
allMCPServers.map((server) => {
|
||||||
<Checkbox checked={isSelected} />
|
const isSelected = tempSelectedMCPIds.includes(
|
||||||
<div className="w-10 h-10 rounded-lg border bg-muted flex items-center justify-center flex-shrink-0">
|
server.uuid || '',
|
||||||
<Server className="h-5 w-5 text-muted-foreground" />
|
);
|
||||||
</div>
|
return (
|
||||||
<div className="flex-1">
|
<div
|
||||||
<div className="font-medium">{server.name}</div>
|
key={server.uuid}
|
||||||
<div className="text-sm text-muted-foreground">
|
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
||||||
{server.mode}
|
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>
|
||||||
{server.runtime_info &&
|
<div className="flex-1">
|
||||||
server.runtime_info.status === 'connected' && (
|
<div className="font-medium">{server.name}</div>
|
||||||
<Badge
|
<div className="text-sm text-muted-foreground">
|
||||||
variant="outline"
|
{server.mode}
|
||||||
className="flex items-center gap-1 mt-1"
|
</div>
|
||||||
>
|
{server.runtime_info &&
|
||||||
<Wrench className="h-3 w-3 text-black dark:text-white" />
|
server.runtime_info.status === 'connected' && (
|
||||||
<span className="text-xs text-black dark:text-white">
|
<Badge
|
||||||
{t('pipelines.extensions.toolCount', {
|
variant="outline"
|
||||||
count: server.runtime_info.tool_count || 0,
|
className="flex items-center gap-1 mt-1"
|
||||||
})}
|
>
|
||||||
</span>
|
<Wrench className="h-3 w-3 text-black dark:text-white" />
|
||||||
</Badge>
|
<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>
|
||||||
{!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 h-[calc(100vh-16rem)] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] 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,6 +57,7 @@ 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[] = [
|
||||||
@@ -262,120 +263,163 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
}, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]);
|
}, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]);
|
||||||
|
|
||||||
// 监听滚动事件
|
// Check if content fills the viewport and load more if needed
|
||||||
|
const checkAndLoadMore = useCallback(() => {
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
if (!scrollContainer || isLoading || isLoadingMore || !hasMore) return;
|
||||||
|
|
||||||
|
const { scrollHeight, clientHeight } = scrollContainer;
|
||||||
|
// If content doesn't fill the viewport (no scrollbar), load more
|
||||||
|
if (scrollHeight <= clientHeight) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
}, [loadMore, isLoading, isLoadingMore, hasMore]);
|
||||||
|
|
||||||
|
// Listen to scroll events on the scroll container
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (
|
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
||||||
window.innerHeight + document.documentElement.scrollTop >=
|
// Load more when scrolled to within 100px of the bottom
|
||||||
document.documentElement.offsetHeight - 100
|
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||||
) {
|
|
||||||
loadMore();
|
loadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
scrollContainer.addEventListener('scroll', handleScroll);
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
||||||
}, [loadMore]);
|
}, [loadMore]);
|
||||||
|
|
||||||
|
// Check if we need to load more after content changes or initial load
|
||||||
|
useEffect(() => {
|
||||||
|
// Small delay to ensure DOM has updated
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
checkAndLoadMore();
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [plugins, checkAndLoadMore]);
|
||||||
|
|
||||||
|
// Also check on window resize
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
checkAndLoadMore();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, [checkAndLoadMore]);
|
||||||
|
|
||||||
// 安装插件
|
// 安装插件
|
||||||
// const handleInstallPlugin = (plugin: PluginV4) => {
|
// const handleInstallPlugin = (plugin: PluginV4) => {
|
||||||
// console.log('install plugin', plugin);
|
// console.log('install plugin', plugin);
|
||||||
// };
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-3 sm:px-4 py-4 sm:py-6 space-y-4 sm:space-y-6">
|
<div className="h-full flex flex-col">
|
||||||
{/* 搜索框 */}
|
{/* Fixed header with search and sort controls */}
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex-shrink-0 space-y-4 px-3 sm:px-4 py-4 sm:py-6">
|
||||||
<div className="relative w-full max-w-2xl">
|
{/* Search box */}
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
<div className="flex items-center justify-center">
|
||||||
<Input
|
<div className="relative w-full max-w-2xl">
|
||||||
placeholder={t('market.searchPlaceholder')}
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||||
value={searchQuery}
|
<Input
|
||||||
onChange={(e) => handleSearchInputChange(e.target.value)}
|
placeholder={t('market.searchPlaceholder')}
|
||||||
onKeyPress={(e) => {
|
value={searchQuery}
|
||||||
if (e.key === 'Enter') {
|
onChange={(e) => handleSearchInputChange(e.target.value)}
|
||||||
// 立即搜索,清除防抖定时器
|
onKeyPress={(e) => {
|
||||||
if (searchTimeoutRef.current) {
|
if (e.key === 'Enter') {
|
||||||
clearTimeout(searchTimeoutRef.current);
|
// Immediately search, clear debounce timer
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
handleSearch(searchQuery);
|
||||||
}
|
}
|
||||||
handleSearch(searchQuery);
|
}}
|
||||||
}
|
className="pl-10 pr-4 text-sm sm:text-base"
|
||||||
}}
|
/>
|
||||||
className="pl-10 pr-4 text-sm sm:text-base"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 排序下拉框 */}
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 搜索结果统计 */}
|
|
||||||
{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>
|
</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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 加载更多指示器 */}
|
{/* Sort dropdown */}
|
||||||
{isLoadingMore && (
|
<div className="flex items-center justify-center">
|
||||||
<div className="flex items-center justify-center py-6">
|
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
||||||
<span className="ml-2">{t('market.loadingMore')}</span>
|
{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>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 没有更多数据提示 */}
|
{/* Search results stats */}
|
||||||
{!hasMore && plugins.length > 0 && (
|
{total > 0 && (
|
||||||
<div className="text-center text-muted-foreground py-6">
|
<div className="text-center text-muted-foreground text-sm">
|
||||||
{t('market.allLoaded')}
|
{searchQuery
|
||||||
</div>
|
? t('market.searchResults', { count: total })
|
||||||
)}
|
: t('market.totalPlugins', { count: total })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 插件详情对话框 */}
|
{/* Scrollable content area */}
|
||||||
|
<div
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
className="flex-1 overflow-y-auto px-3 sm:px-4"
|
||||||
|
>
|
||||||
|
{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 pb-6 pt-4">
|
||||||
|
{plugins.map((plugin) => (
|
||||||
|
<PluginMarketCardComponent
|
||||||
|
key={plugin.pluginId}
|
||||||
|
cardVO={plugin}
|
||||||
|
onPluginClick={handlePluginClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading more indicator */}
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{/* Plugin detail dialog */}
|
||||||
<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 px-[0.8rem] pt-[0rem] gap-4">
|
<div className="w-full h-full px-[0.8rem] pt-[0rem]">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] 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 h-[calc(100vh-16rem)] w-full gap-2">
|
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] 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]">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pt-[2rem] pb-6">
|
||||||
{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} ${isDragOver ? 'bg-blue-50' : ''}`}
|
className={`${styles.pageContainer} h-full flex flex-col ${isDragOver ? 'bg-blue-50' : ''}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -443,8 +443,12 @@ export default function PluginConfigPage() {
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs
|
||||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
value={activeTab}
|
||||||
|
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')}
|
||||||
@@ -522,10 +526,10 @@ export default function PluginConfigPage() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TabsContent value="installed">
|
<TabsContent value="installed" className="flex-1 overflow-y-auto mt-0">
|
||||||
<PluginInstalledComponent ref={pluginInstalledRef} />
|
<PluginInstalledComponent ref={pluginInstalledRef} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="market">
|
<TabsContent value="market" className="flex-1 overflow-y-auto mt-0">
|
||||||
<MarketPage
|
<MarketPage
|
||||||
installPlugin={(plugin: PluginV4) => {
|
installPlugin={(plugin: PluginV4) => {
|
||||||
setInstallSource('marketplace');
|
setInstallSource('marketplace');
|
||||||
@@ -539,7 +543,10 @@ export default function PluginConfigPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="mcp-servers">
|
<TabsContent
|
||||||
|
value="mcp-servers"
|
||||||
|
className="flex-1 overflow-y-auto mt-0"
|
||||||
|
>
|
||||||
<MCPServerComponent
|
<MCPServerComponent
|
||||||
key={refreshKey}
|
key={refreshKey}
|
||||||
onEditServer={(serverName) => {
|
onEditServer={(serverName) => {
|
||||||
|
|||||||
@@ -482,6 +482,9 @@ 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',
|
||||||
|
selectAll: 'Select All',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'Pipeline Chat',
|
title: 'Pipeline Chat',
|
||||||
|
|||||||
@@ -485,6 +485,9 @@ const jaJP = {
|
|||||||
addMCPServer: 'MCPサーバーを追加',
|
addMCPServer: 'MCPサーバーを追加',
|
||||||
selectMCPServers: 'MCPサーバーを選択',
|
selectMCPServers: 'MCPサーバーを選択',
|
||||||
toolCount: '{{count}}個のツール',
|
toolCount: '{{count}}個のツール',
|
||||||
|
noPluginsInstalled: 'インストールされているプラグインがありません',
|
||||||
|
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
||||||
|
selectAll: 'すべて選択',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'パイプラインのチャット',
|
title: 'パイプラインのチャット',
|
||||||
|
|||||||
@@ -464,6 +464,9 @@ const zhHans = {
|
|||||||
addMCPServer: '添加 MCP 服务器',
|
addMCPServer: '添加 MCP 服务器',
|
||||||
selectMCPServers: '选择 MCP 服务器',
|
selectMCPServers: '选择 MCP 服务器',
|
||||||
toolCount: '{{count}} 个工具',
|
toolCount: '{{count}} 个工具',
|
||||||
|
noPluginsInstalled: '无已安装的插件',
|
||||||
|
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
||||||
|
selectAll: '全选',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流水线对话',
|
title: '流水线对话',
|
||||||
|
|||||||
@@ -462,6 +462,9 @@ const zhHant = {
|
|||||||
addMCPServer: '新增 MCP 伺服器',
|
addMCPServer: '新增 MCP 伺服器',
|
||||||
selectMCPServers: '選擇 MCP 伺服器',
|
selectMCPServers: '選擇 MCP 伺服器',
|
||||||
toolCount: '{{count}} 個工具',
|
toolCount: '{{count}} 個工具',
|
||||||
|
noPluginsInstalled: '無已安裝的插件',
|
||||||
|
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
||||||
|
selectAll: '全選',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流程線對話',
|
title: '流程線對話',
|
||||||
|
|||||||
Reference in New Issue
Block a user