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:
|
||||
>
|
||||
|
||||
### 更改前后对比截图 / Screenshots
|
||||
|
||||
> 请在此部分粘贴更改前后对比截图(可以是界面截图、控制台输出、对话截图等):
|
||||
> Please paste the screenshots of changes before and after here (can be interface screenshots, console output, conversation screenshots, etc.):
|
||||
>
|
||||
> 修改前 / Before:
|
||||
>
|
||||
> 修改后 / After:
|
||||
>
|
||||
|
||||
## 检查清单 / Checklist
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "langbot"
|
||||
version = "4.5.2"
|
||||
version = "4.5.3"
|
||||
description = "Easy-to-use global IM bot platform designed for LLM era"
|
||||
readme = "README.md"
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""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):
|
||||
if not await self.check_access_token():
|
||||
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:
|
||||
params = {
|
||||
'touser': user_id,
|
||||
'toparty': '',
|
||||
'totag': '',
|
||||
'agentid': agent_id,
|
||||
'msgtype': 'image',
|
||||
'agentid': agent_id,
|
||||
'image': {
|
||||
'media_id': media_id,
|
||||
},
|
||||
@@ -125,19 +124,13 @@ class WecomClient:
|
||||
'enable_duplicate_check': 0,
|
||||
'duplicate_check_interval': 1800,
|
||||
}
|
||||
try:
|
||||
response = await client.post(url, json=params)
|
||||
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:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
return await self.send_image(user_id, agent_id, media_id)
|
||||
|
||||
if data['errcode'] != 0:
|
||||
await self.logger.error(f'发送图片失败:{data}')
|
||||
raise Exception('Failed to send image: ' + str(data))
|
||||
|
||||
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
|
||||
"""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')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-md">
|
||||
<div className="border rounded-md overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t('common.name')}</TableHead>
|
||||
<TableHead>{t('common.apiKeyValue')}</TableHead>
|
||||
<TableHead className="min-w-[120px]">
|
||||
{t('common.name')}
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[200px]">
|
||||
{t('common.apiKeyValue')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px]">
|
||||
{t('common.actions')}
|
||||
</TableHead>
|
||||
@@ -372,16 +376,20 @@ export default function ApiIntegrationDialog({
|
||||
{t('common.noWebhooks')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<div className="border rounded-md overflow-x-auto max-w-full">
|
||||
<Table className="table-fixed w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t('common.name')}</TableHead>
|
||||
<TableHead>{t('common.webhookUrl')}</TableHead>
|
||||
<TableHead className="w-[150px]">
|
||||
{t('common.name')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[380px]">
|
||||
{t('common.webhookUrl')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[80px]">
|
||||
{t('common.webhookEnabled')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px]">
|
||||
<TableHead className="w-[80px]">
|
||||
{t('common.actions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -389,20 +397,30 @@ export default function ApiIntegrationDialog({
|
||||
<TableBody>
|
||||
{webhooks.map((webhook) => (
|
||||
<TableRow key={webhook.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{webhook.name}</div>
|
||||
<TableCell className="truncate">
|
||||
<div className="truncate">
|
||||
<div
|
||||
className="font-medium truncate"
|
||||
title={webhook.name}
|
||||
>
|
||||
{webhook.name}
|
||||
</div>
|
||||
{webhook.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div
|
||||
className="text-sm text-muted-foreground truncate"
|
||||
title={webhook.description}
|
||||
>
|
||||
{webhook.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded break-all">
|
||||
<div className="overflow-x-auto max-w-[380px]">
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded whitespace-nowrap inline-block">
|
||||
{webhook.url}
|
||||
</code>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
|
||||
@@ -240,7 +240,7 @@ export default function DynamicFormItemComponent({
|
||||
model.requester,
|
||||
)}
|
||||
alt="icon"
|
||||
className="w-8 h-8 rounded-full"
|
||||
className="w-8 h-8 rounded-[8%]"
|
||||
/>
|
||||
<h4 className="font-medium">{model.name}</h4>
|
||||
</div>
|
||||
|
||||
@@ -188,40 +188,6 @@ export default function HomeSidebar({
|
||||
</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
|
||||
onClick={() => {
|
||||
setApiKeyDialogOpen(true);
|
||||
@@ -302,6 +268,41 @@ export default function HomeSidebar({
|
||||
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<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
|
||||
variant="ghost"
|
||||
className="w-full justify-start font-normal"
|
||||
|
||||
@@ -235,7 +235,7 @@ export default function KBForm({
|
||||
model.requester,
|
||||
)}
|
||||
alt="icon"
|
||||
className="w-8 h-8 rounded-full"
|
||||
className="w-8 h-8 rounded-[8%]"
|
||||
/>
|
||||
<h4 className="font-medium">
|
||||
{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 newSelected = allPlugins.filter((p) =>
|
||||
tempSelectedPluginIds.includes(getPluginId(p)),
|
||||
@@ -330,8 +350,32 @@ export default function PipelineExtension({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
||||
</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">
|
||||
{allPlugins.map((plugin) => {
|
||||
{allPlugins.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('pipelines.extensions.noPluginsInstalled')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
allPlugins.map((plugin) => {
|
||||
const pluginId = getPluginId(plugin);
|
||||
const metadata = plugin.manifest.manifest.metadata;
|
||||
const isSelected = tempSelectedPluginIds.includes(pluginId);
|
||||
@@ -372,7 +416,8 @@ export default function PipelineExtension({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
@@ -396,9 +441,35 @@ export default function PipelineExtension({
|
||||
{t('pipelines.extensions.selectMCPServers')}
|
||||
</DialogTitle>
|
||||
</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">
|
||||
{allMCPServers.map((server) => {
|
||||
const isSelected = tempSelectedMCPIds.includes(server.uuid || '');
|
||||
{allMCPServers.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('pipelines.extensions.noMCPServersConfigured')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
allMCPServers.map((server) => {
|
||||
const isSelected = tempSelectedMCPIds.includes(
|
||||
server.uuid || '',
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={server.uuid}
|
||||
@@ -436,7 +507,8 @@ export default function PipelineExtension({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setMcpDialogOpen(false)}>
|
||||
|
||||
@@ -274,7 +274,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
</Dialog>
|
||||
|
||||
{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
|
||||
className="h-[3rem] w-[3rem]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -57,6 +57,7 @@ function MarketPageContent({
|
||||
|
||||
const pageSize = 16; // 每页16个,4行x4列
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// 排序选项
|
||||
const sortOptions: SortOption[] = [
|
||||
@@ -262,29 +263,64 @@ function MarketPageContent({
|
||||
}
|
||||
}, [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(() => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (!scrollContainer) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (
|
||||
window.innerHeight + document.documentElement.scrollTop >=
|
||||
document.documentElement.offsetHeight - 100
|
||||
) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
||||
// Load more when scrolled to within 100px of the bottom
|
||||
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||
loadMore();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
scrollContainer.addEventListener('scroll', handleScroll);
|
||||
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
||||
}, [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) => {
|
||||
// console.log('install plugin', plugin);
|
||||
// };
|
||||
|
||||
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-shrink-0 space-y-4 px-3 sm:px-4 py-4 sm:py-6">
|
||||
{/* Search box */}
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
@@ -294,7 +330,7 @@ function MarketPageContent({
|
||||
onChange={(e) => handleSearchInputChange(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// 立即搜索,清除防抖定时器
|
||||
// Immediately search, clear debounce timer
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
@@ -306,7 +342,7 @@ function MarketPageContent({
|
||||
</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">
|
||||
@@ -327,7 +363,7 @@ function MarketPageContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索结果统计 */}
|
||||
{/* Search results stats */}
|
||||
{total > 0 && (
|
||||
<div className="text-center text-muted-foreground text-sm">
|
||||
{searchQuery
|
||||
@@ -335,8 +371,13 @@ function MarketPageContent({
|
||||
: 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" />
|
||||
@@ -349,7 +390,8 @@ function MarketPageContent({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6">
|
||||
<>
|
||||
<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}
|
||||
@@ -358,9 +400,8 @@ function MarketPageContent({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 加载更多指示器 */}
|
||||
{/* Loading more indicator */}
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
@@ -368,14 +409,17 @@ function MarketPageContent({
|
||||
</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
|
||||
open={dialogOpen}
|
||||
onOpenChange={handleDialogClose}
|
||||
|
||||
@@ -73,14 +73,14 @@ export default function MCPComponent({
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
{/* 已安装的服务器列表 */}
|
||||
<div className="w-full px-[0.8rem] pt-[0rem] gap-4">
|
||||
{/* Server list */}
|
||||
<div className="w-full h-full px-[0.8rem] pt-[0rem]">
|
||||
{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')}
|
||||
</div>
|
||||
) : 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
|
||||
className="h-[3rem] w-[3rem]"
|
||||
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>
|
||||
) : (
|
||||
<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) => (
|
||||
<div key={`${server.name}-${index}`}>
|
||||
<MCPCardComponent
|
||||
|
||||
@@ -431,7 +431,7 @@ export default function PluginConfigPage() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
|
||||
className={`${styles.pageContainer} h-full flex flex-col ${isDragOver ? 'bg-blue-50' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
@@ -443,8 +443,12 @@ export default function PluginConfigPage() {
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
||||
<Tabs
|
||||
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]">
|
||||
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
||||
{t('plugins.installed')}
|
||||
@@ -522,10 +526,10 @@ export default function PluginConfigPage() {
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="installed">
|
||||
<TabsContent value="installed" className="flex-1 overflow-y-auto mt-0">
|
||||
<PluginInstalledComponent ref={pluginInstalledRef} />
|
||||
</TabsContent>
|
||||
<TabsContent value="market">
|
||||
<TabsContent value="market" className="flex-1 overflow-y-auto mt-0">
|
||||
<MarketPage
|
||||
installPlugin={(plugin: PluginV4) => {
|
||||
setInstallSource('marketplace');
|
||||
@@ -539,7 +543,10 @@ export default function PluginConfigPage() {
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="mcp-servers">
|
||||
<TabsContent
|
||||
value="mcp-servers"
|
||||
className="flex-1 overflow-y-auto mt-0"
|
||||
>
|
||||
<MCPServerComponent
|
||||
key={refreshKey}
|
||||
onEditServer={(serverName) => {
|
||||
|
||||
@@ -482,6 +482,9 @@ const enUS = {
|
||||
addMCPServer: 'Add MCP Server',
|
||||
selectMCPServers: 'Select MCP Servers',
|
||||
toolCount: '{{count}} tools',
|
||||
noPluginsInstalled: 'No installed plugins',
|
||||
noMCPServersConfigured: 'No configured MCP servers',
|
||||
selectAll: 'Select All',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Pipeline Chat',
|
||||
|
||||
@@ -485,6 +485,9 @@ const jaJP = {
|
||||
addMCPServer: 'MCPサーバーを追加',
|
||||
selectMCPServers: 'MCPサーバーを選択',
|
||||
toolCount: '{{count}}個のツール',
|
||||
noPluginsInstalled: 'インストールされているプラグインがありません',
|
||||
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
||||
selectAll: 'すべて選択',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'パイプラインのチャット',
|
||||
|
||||
@@ -464,6 +464,9 @@ const zhHans = {
|
||||
addMCPServer: '添加 MCP 服务器',
|
||||
selectMCPServers: '选择 MCP 服务器',
|
||||
toolCount: '{{count}} 个工具',
|
||||
noPluginsInstalled: '无已安装的插件',
|
||||
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
||||
selectAll: '全选',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '流水线对话',
|
||||
|
||||
@@ -462,6 +462,9 @@ const zhHant = {
|
||||
addMCPServer: '新增 MCP 伺服器',
|
||||
selectMCPServers: '選擇 MCP 伺服器',
|
||||
toolCount: '{{count}} 個工具',
|
||||
noPluginsInstalled: '無已安裝的插件',
|
||||
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
||||
selectAll: '全選',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '流程線對話',
|
||||
|
||||
Reference in New Issue
Block a user