feat: Implement extension and bot limitations across services and UI (#1991)

- Added checks for maximum allowed extensions, bots, and pipelines in the backend services (PluginsRouterGroup, BotService, MCPService, PipelineService).
- Updated system configuration to include limitation settings for max_bots, max_pipelines, and max_extensions.
- Enhanced frontend components to handle limitations, providing user feedback when limits are reached.
- Added internationalization support for limitation messages in English, Japanese, Simplified Chinese, and Traditional Chinese.
This commit is contained in:
Junyan Chin
2026-02-22 04:25:45 -05:00
committed by GitHub
parent aa09a27a63
commit 42caae1bcf
17 changed files with 161 additions and 5 deletions

View File

@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
import { extractI18nObject } from '@/i18n/I18nProvider';
import BotDetailDialog from '@/app/home/bots/BotDetailDialog';
import { CustomApiError } from '@/app/infra/entities/common';
import { systemInfo } from '@/app/infra/http';
export default function BotConfigPage() {
const { t } = useTranslation();
@@ -60,6 +61,11 @@ export default function BotConfigPage() {
}
function handleCreateBotClick() {
const maxBots = systemInfo.limitation?.max_bots ?? -1;
if (maxBots >= 0 && botList.length >= maxBots) {
toast.error(t('limitation.maxBotsReached', { max: maxBots }));
return;
}
setSelectedBotId('');
setDetailDialogOpen(true);
}

View File

@@ -15,6 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { systemInfo } from '@/app/infra/http';
export default function PluginConfigPage() {
const { t } = useTranslation();
@@ -87,6 +88,11 @@ export default function PluginConfigPage() {
};
const handleCreateNew = () => {
const maxPipelines = systemInfo.limitation?.max_pipelines ?? -1;
if (maxPipelines >= 0 && pipelineList.length >= maxPipelines) {
toast.error(t('limitation.maxPipelinesReached', { max: maxPipelines }));
return;
}
setIsEditForm(false);
setSelectedPipelineId('');
setDialogOpen(true);

View File

@@ -453,7 +453,10 @@ export default function MCPFormDialog({
onSuccess?.();
} catch (error) {
console.error('Failed to save MCP server:', error);
toast.error(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed'));
const errMsg = (error as CustomApiError).msg || '';
toast.error(
(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed')) + errMsg,
);
}
}

View File

@@ -186,6 +186,28 @@ export default function PluginConfigPage() {
setFetchingAssets(false);
}
async function checkExtensionsLimit(): Promise<boolean> {
const maxExtensions = systemInfo.limitation?.max_extensions ?? -1;
if (maxExtensions < 0) return true;
try {
const [pluginsResp, mcpResp] = await Promise.all([
httpClient.getPlugins(),
httpClient.getMCPServers(),
]);
const total =
(pluginsResp.plugins?.length ?? 0) + (mcpResp.servers?.length ?? 0);
if (total >= maxExtensions) {
toast.error(
t('limitation.maxExtensionsReached', { max: maxExtensions }),
);
return false;
}
} catch {
// If we can't check, let backend handle it
}
return true;
}
async function fetchGithubReleases() {
if (!githubURL.trim()) {
toast.error(t('plugins.enterRepoUrl'));
@@ -328,6 +350,8 @@ export default function PluginConfigPage() {
return;
}
if (!(await checkExtensionsLimit())) return;
setModalOpen(true);
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
setInstallError(null);
@@ -336,7 +360,8 @@ export default function PluginConfigPage() {
[t, pluginSystemStatus, installPlugin],
);
const handleFileSelect = useCallback(() => {
const handleFileSelect = useCallback(async () => {
if (!(await checkExtensionsLimit())) return;
if (fileInputRef.current) {
fileInputRef.current.click();
}
@@ -633,7 +658,8 @@ export default function PluginConfigPage() {
{activeTab === 'mcp-servers' ? (
<>
<DropdownMenuItem
onClick={() => {
onClick={async () => {
if (!(await checkExtensionsLimit())) return;
setActiveTab('mcp-servers');
setIsEditMode(false);
setEditingServerName(null);
@@ -661,7 +687,8 @@ export default function PluginConfigPage() {
{t('plugins.uploadLocal')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
onClick={async () => {
if (!(await checkExtensionsLimit())) return;
setInstallSource('github');
setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT);
setInstallError(null);
@@ -683,7 +710,8 @@ export default function PluginConfigPage() {
</TabsContent>
<TabsContent value="market" className="flex-1 overflow-y-auto mt-0">
<MarketPage
installPlugin={(plugin: PluginV4) => {
installPlugin={async (plugin: PluginV4) => {
if (!(await checkExtensionsLimit())) return;
setInstallSource('marketplace');
setInstallInfo({
plugin_author: plugin.author,

View File

@@ -240,13 +240,21 @@ export interface PluginReorderElement {
}
// system
export interface SystemLimitation {
max_bots: number;
max_pipelines: number;
max_extensions: number;
}
export interface ApiRespSystemInfo {
debug: boolean;
version: string;
edition: string;
cloud_service_url: string;
enable_marketplace: boolean;
allow_modify_login_info: boolean;
disable_models_service: boolean;
limitation: SystemLimitation;
}
export interface ApiRespPluginSystemStatus {

View File

@@ -6,10 +6,16 @@ import { ApiRespSystemInfo } from '@/app/infra/entities/api';
export let systemInfo: ApiRespSystemInfo = {
debug: false,
version: '',
edition: 'community',
enable_marketplace: true,
cloud_service_url: '',
allow_modify_login_info: true,
disable_models_service: false,
limitation: {
max_bots: -1,
max_pipelines: -1,
max_extensions: -1,
},
};
// 用户信息

View File

@@ -962,6 +962,14 @@ const enUS = {
sessions: 'Sessions',
},
},
limitation: {
maxBotsReached:
'Maximum number of bots ({{max}}) reached. Please remove an existing bot before creating a new one.',
maxPipelinesReached:
'Maximum number of pipelines ({{max}}) reached. Please remove an existing pipeline before creating a new one.',
maxExtensionsReached:
'Maximum number of extensions ({{max}}) reached. Please remove an existing MCP server or plugin before adding a new one.',
},
};
export default enUS;

View File

@@ -949,6 +949,14 @@ const jaJP = {
sessions: 'セッション',
},
},
limitation: {
maxBotsReached:
'ボット数が上限({{max}}個)に達しました。新しいボットを作成するには、既存のボットを削除してください。',
maxPipelinesReached:
'パイプライン数が上限({{max}}個)に達しました。新しいパイプラインを作成するには、既存のパイプラインを削除してください。',
maxExtensionsReached:
'拡張機能数が上限({{max}}個)に達しました。新しい MCP サーバーやプラグインを追加するには、既存のものを削除してください。',
},
};
export default jaJP;

View File

@@ -922,6 +922,14 @@ const zhHans = {
sessions: '会话记录',
},
},
limitation: {
maxBotsReached:
'已达到机器人数量上限({{max}}个)。请先删除已有机器人后再创建新的。',
maxPipelinesReached:
'已达到流水线数量上限({{max}}个)。请先删除已有流水线后再创建新的。',
maxExtensionsReached:
'已达到扩展数量上限({{max}}个)。请先删除已有的 MCP 服务器或插件后再添加新的。',
},
};
export default zhHans;

View File

@@ -897,6 +897,14 @@ const zhHant = {
sessions: '會話記錄',
},
},
limitation: {
maxBotsReached:
'已達到機器人數量上限({{max}}個)。請先刪除已有機器人後再建立新的。',
maxPipelinesReached:
'已達到流水線數量上限({{max}}個)。請先刪除已有流水線後再建立新的。',
maxExtensionsReached:
'已達到擴充功能數量上限({{max}}個)。請先刪除已有的 MCP 伺服器或外掛後再新增。',
},
};
export default zhHant;