mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
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:
@@ -14,6 +14,18 @@ from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
|||||||
|
|
||||||
@group.group_class('plugins', '/api/v1/plugins')
|
@group.group_class('plugins', '/api/v1/plugins')
|
||||||
class PluginsRouterGroup(group.RouterGroup):
|
class PluginsRouterGroup(group.RouterGroup):
|
||||||
|
async def _check_extensions_limit(self) -> str | None:
|
||||||
|
"""Check if extensions limit is reached. Returns error response if limit exceeded, None otherwise."""
|
||||||
|
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||||
|
max_extensions = limitation.get('max_extensions', -1)
|
||||||
|
if max_extensions >= 0:
|
||||||
|
plugins = await self.ap.plugin_connector.list_plugins()
|
||||||
|
mcp_servers = await self.ap.mcp_service.get_mcp_servers()
|
||||||
|
total_extensions = len(plugins) + len(mcp_servers)
|
||||||
|
if total_extensions >= max_extensions:
|
||||||
|
return self.http_status(400, -1, f'Maximum number of extensions ({max_extensions}) reached')
|
||||||
|
return None
|
||||||
|
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
@@ -239,6 +251,10 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
"""Install plugin from GitHub release asset"""
|
"""Install plugin from GitHub release asset"""
|
||||||
|
limit_error = await self._check_extensions_limit()
|
||||||
|
if limit_error is not None:
|
||||||
|
return limit_error
|
||||||
|
|
||||||
data = await quart.request.json
|
data = await quart.request.json
|
||||||
asset_url = data.get('asset_url', '')
|
asset_url = data.get('asset_url', '')
|
||||||
owner = data.get('owner', '')
|
owner = data.get('owner', '')
|
||||||
@@ -273,6 +289,10 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||||
)
|
)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
|
limit_error = await self._check_extensions_limit()
|
||||||
|
if limit_error is not None:
|
||||||
|
return limit_error
|
||||||
|
|
||||||
data = await quart.request.json
|
data = await quart.request.json
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
ctx = taskmgr.TaskContext.new()
|
||||||
@@ -288,6 +308,10 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
|
|
||||||
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
|
limit_error = await self._check_extensions_limit()
|
||||||
|
if limit_error is not None:
|
||||||
|
return limit_error
|
||||||
|
|
||||||
file = (await quart.request.files).get('file')
|
file = (await quart.request.files).get('file')
|
||||||
if file is None:
|
if file is None:
|
||||||
return self.http_status(400, -1, 'file is required')
|
return self.http_status(400, -1, 'file is required')
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class SystemRouterGroup(group.RouterGroup):
|
|||||||
data={
|
data={
|
||||||
'version': constants.semantic_version,
|
'version': constants.semantic_version,
|
||||||
'debug': constants.debug_mode,
|
'debug': constants.debug_mode,
|
||||||
|
'edition': constants.edition,
|
||||||
'enable_marketplace': self.ap.instance_config.data.get('plugin', {}).get(
|
'enable_marketplace': self.ap.instance_config.data.get('plugin', {}).get(
|
||||||
'enable_marketplace', True
|
'enable_marketplace', True
|
||||||
),
|
),
|
||||||
@@ -25,6 +26,7 @@ class SystemRouterGroup(group.RouterGroup):
|
|||||||
'disable_models_service': self.ap.instance_config.data.get('space', {}).get(
|
'disable_models_service': self.ap.instance_config.data.get('space', {}).get(
|
||||||
'disable_models_service', False
|
'disable_models_service', False
|
||||||
),
|
),
|
||||||
|
'limitation': self.ap.instance_config.data.get('system', {}).get('limitation', {}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ class BotService:
|
|||||||
|
|
||||||
async def create_bot(self, bot_data: dict) -> str:
|
async def create_bot(self, bot_data: dict) -> str:
|
||||||
"""Create bot"""
|
"""Create bot"""
|
||||||
|
# Check limitation
|
||||||
|
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||||
|
max_bots = limitation.get('max_bots', -1)
|
||||||
|
if max_bots >= 0:
|
||||||
|
existing_bots = await self.get_bots()
|
||||||
|
if len(existing_bots) >= max_bots:
|
||||||
|
raise ValueError(f'Maximum number of bots ({max_bots}) reached')
|
||||||
|
|
||||||
# TODO: 检查配置信息格式
|
# TODO: 检查配置信息格式
|
||||||
bot_data['uuid'] = str(uuid.uuid4())
|
bot_data['uuid'] = str(uuid.uuid4())
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ class MCPService:
|
|||||||
return serialized_servers
|
return serialized_servers
|
||||||
|
|
||||||
async def create_mcp_server(self, server_data: dict) -> str:
|
async def create_mcp_server(self, server_data: dict) -> str:
|
||||||
|
# Check limitation (extensions = MCP servers + plugins)
|
||||||
|
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||||
|
max_extensions = limitation.get('max_extensions', -1)
|
||||||
|
if max_extensions >= 0:
|
||||||
|
existing_mcp_servers = await self.get_mcp_servers()
|
||||||
|
plugins = await self.ap.plugin_connector.list_plugins()
|
||||||
|
total_extensions = len(existing_mcp_servers) + len(plugins)
|
||||||
|
if total_extensions >= max_extensions:
|
||||||
|
raise ValueError(f'Maximum number of extensions ({max_extensions}) reached')
|
||||||
|
|
||||||
server_data['uuid'] = str(uuid.uuid4())
|
server_data['uuid'] = str(uuid.uuid4())
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
|
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ class PipelineService:
|
|||||||
async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str:
|
async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str:
|
||||||
from ....utils import paths as path_utils
|
from ....utils import paths as path_utils
|
||||||
|
|
||||||
|
# Check limitation
|
||||||
|
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||||
|
max_pipelines = limitation.get('max_pipelines', -1)
|
||||||
|
if max_pipelines >= 0:
|
||||||
|
existing_pipelines = await self.get_pipelines()
|
||||||
|
if len(existing_pipelines) >= max_pipelines:
|
||||||
|
raise ValueError(f'Maximum number of pipelines ({max_pipelines}) reached')
|
||||||
|
|
||||||
pipeline_data['uuid'] = str(uuid.uuid4())
|
pipeline_data['uuid'] = str(uuid.uuid4())
|
||||||
pipeline_data['for_version'] = self.ap.ver_mgr.get_current_version()
|
pipeline_data['for_version'] = self.ap.ver_mgr.get_current_version()
|
||||||
pipeline_data['stages'] = default_stage_order.copy()
|
pipeline_data['stages'] = default_stage_order.copy()
|
||||||
@@ -153,6 +161,14 @@ class PipelineService:
|
|||||||
|
|
||||||
async def copy_pipeline(self, pipeline_uuid: str) -> str:
|
async def copy_pipeline(self, pipeline_uuid: str) -> str:
|
||||||
"""Copy a pipeline with all its configurations"""
|
"""Copy a pipeline with all its configurations"""
|
||||||
|
# Check limitation
|
||||||
|
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
|
||||||
|
max_pipelines = limitation.get('max_pipelines', -1)
|
||||||
|
if max_pipelines >= 0:
|
||||||
|
existing_pipelines = await self.get_pipelines()
|
||||||
|
if len(existing_pipelines) >= max_pipelines:
|
||||||
|
raise ValueError(f'Maximum number of pipelines ({max_pipelines}) reached')
|
||||||
|
|
||||||
# Get the original pipeline
|
# Get the original pipeline
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
result = await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
||||||
|
|||||||
@@ -156,8 +156,10 @@ class LoadConfigStage(stage.BootingStage):
|
|||||||
)
|
)
|
||||||
|
|
||||||
constants.instance_id = ap.instance_id.data['instance_id']
|
constants.instance_id = ap.instance_id.data['instance_id']
|
||||||
|
constants.edition = ap.instance_config.data.get('system', {}).get('edition', 'community')
|
||||||
|
|
||||||
print(f'LangBot instance id: {constants.instance_id}')
|
print(f'LangBot instance id: {constants.instance_id}')
|
||||||
|
print(f'LangBot edition: {constants.edition}')
|
||||||
|
|
||||||
await ap.instance_id.dump_config()
|
await ap.instance_id.dump_config()
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,13 @@ proxy:
|
|||||||
http: ''
|
http: ''
|
||||||
https: ''
|
https: ''
|
||||||
system:
|
system:
|
||||||
|
edition: community
|
||||||
recovery_key: ''
|
recovery_key: ''
|
||||||
allow_modify_login_info: true
|
allow_modify_login_info: true
|
||||||
|
limitation:
|
||||||
|
max_bots: -1
|
||||||
|
max_pipelines: -1
|
||||||
|
max_extensions: -1
|
||||||
jwt:
|
jwt:
|
||||||
expire: 604800
|
expire: 604800
|
||||||
secret: ''
|
secret: ''
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import BotDetailDialog from '@/app/home/bots/BotDetailDialog';
|
import BotDetailDialog from '@/app/home/bots/BotDetailDialog';
|
||||||
import { CustomApiError } from '@/app/infra/entities/common';
|
import { CustomApiError } from '@/app/infra/entities/common';
|
||||||
|
import { systemInfo } from '@/app/infra/http';
|
||||||
|
|
||||||
export default function BotConfigPage() {
|
export default function BotConfigPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -60,6 +61,11 @@ export default function BotConfigPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateBotClick() {
|
function handleCreateBotClick() {
|
||||||
|
const maxBots = systemInfo.limitation?.max_bots ?? -1;
|
||||||
|
if (maxBots >= 0 && botList.length >= maxBots) {
|
||||||
|
toast.error(t('limitation.maxBotsReached', { max: maxBots }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSelectedBotId('');
|
setSelectedBotId('');
|
||||||
setDetailDialogOpen(true);
|
setDetailDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import { systemInfo } from '@/app/infra/http';
|
||||||
|
|
||||||
export default function PluginConfigPage() {
|
export default function PluginConfigPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -87,6 +88,11 @@ export default function PluginConfigPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateNew = () => {
|
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);
|
setIsEditForm(false);
|
||||||
setSelectedPipelineId('');
|
setSelectedPipelineId('');
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
|
|||||||
@@ -453,7 +453,10 @@ export default function MCPFormDialog({
|
|||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save MCP server:', 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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,28 @@ export default function PluginConfigPage() {
|
|||||||
setFetchingAssets(false);
|
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() {
|
async function fetchGithubReleases() {
|
||||||
if (!githubURL.trim()) {
|
if (!githubURL.trim()) {
|
||||||
toast.error(t('plugins.enterRepoUrl'));
|
toast.error(t('plugins.enterRepoUrl'));
|
||||||
@@ -328,6 +350,8 @@ export default function PluginConfigPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await checkExtensionsLimit())) return;
|
||||||
|
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
|
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
|
||||||
setInstallError(null);
|
setInstallError(null);
|
||||||
@@ -336,7 +360,8 @@ export default function PluginConfigPage() {
|
|||||||
[t, pluginSystemStatus, installPlugin],
|
[t, pluginSystemStatus, installPlugin],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFileSelect = useCallback(() => {
|
const handleFileSelect = useCallback(async () => {
|
||||||
|
if (!(await checkExtensionsLimit())) return;
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.click();
|
fileInputRef.current.click();
|
||||||
}
|
}
|
||||||
@@ -633,7 +658,8 @@ export default function PluginConfigPage() {
|
|||||||
{activeTab === 'mcp-servers' ? (
|
{activeTab === 'mcp-servers' ? (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
|
if (!(await checkExtensionsLimit())) return;
|
||||||
setActiveTab('mcp-servers');
|
setActiveTab('mcp-servers');
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
setEditingServerName(null);
|
setEditingServerName(null);
|
||||||
@@ -661,7 +687,8 @@ export default function PluginConfigPage() {
|
|||||||
{t('plugins.uploadLocal')}
|
{t('plugins.uploadLocal')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
|
if (!(await checkExtensionsLimit())) return;
|
||||||
setInstallSource('github');
|
setInstallSource('github');
|
||||||
setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT);
|
setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT);
|
||||||
setInstallError(null);
|
setInstallError(null);
|
||||||
@@ -683,7 +710,8 @@ export default function PluginConfigPage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="market" className="flex-1 overflow-y-auto mt-0">
|
<TabsContent value="market" className="flex-1 overflow-y-auto mt-0">
|
||||||
<MarketPage
|
<MarketPage
|
||||||
installPlugin={(plugin: PluginV4) => {
|
installPlugin={async (plugin: PluginV4) => {
|
||||||
|
if (!(await checkExtensionsLimit())) return;
|
||||||
setInstallSource('marketplace');
|
setInstallSource('marketplace');
|
||||||
setInstallInfo({
|
setInstallInfo({
|
||||||
plugin_author: plugin.author,
|
plugin_author: plugin.author,
|
||||||
|
|||||||
@@ -240,13 +240,21 @@ export interface PluginReorderElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// system
|
// system
|
||||||
|
export interface SystemLimitation {
|
||||||
|
max_bots: number;
|
||||||
|
max_pipelines: number;
|
||||||
|
max_extensions: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiRespSystemInfo {
|
export interface ApiRespSystemInfo {
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
|
edition: string;
|
||||||
cloud_service_url: string;
|
cloud_service_url: string;
|
||||||
enable_marketplace: boolean;
|
enable_marketplace: boolean;
|
||||||
allow_modify_login_info: boolean;
|
allow_modify_login_info: boolean;
|
||||||
disable_models_service: boolean;
|
disable_models_service: boolean;
|
||||||
|
limitation: SystemLimitation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiRespPluginSystemStatus {
|
export interface ApiRespPluginSystemStatus {
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ import { ApiRespSystemInfo } from '@/app/infra/entities/api';
|
|||||||
export let systemInfo: ApiRespSystemInfo = {
|
export let systemInfo: ApiRespSystemInfo = {
|
||||||
debug: false,
|
debug: false,
|
||||||
version: '',
|
version: '',
|
||||||
|
edition: 'community',
|
||||||
enable_marketplace: true,
|
enable_marketplace: true,
|
||||||
cloud_service_url: '',
|
cloud_service_url: '',
|
||||||
allow_modify_login_info: true,
|
allow_modify_login_info: true,
|
||||||
disable_models_service: false,
|
disable_models_service: false,
|
||||||
|
limitation: {
|
||||||
|
max_bots: -1,
|
||||||
|
max_pipelines: -1,
|
||||||
|
max_extensions: -1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
|
|||||||
@@ -962,6 +962,14 @@ const enUS = {
|
|||||||
sessions: 'Sessions',
|
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;
|
export default enUS;
|
||||||
|
|||||||
@@ -949,6 +949,14 @@ const jaJP = {
|
|||||||
sessions: 'セッション',
|
sessions: 'セッション',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
limitation: {
|
||||||
|
maxBotsReached:
|
||||||
|
'ボット数が上限({{max}}個)に達しました。新しいボットを作成するには、既存のボットを削除してください。',
|
||||||
|
maxPipelinesReached:
|
||||||
|
'パイプライン数が上限({{max}}個)に達しました。新しいパイプラインを作成するには、既存のパイプラインを削除してください。',
|
||||||
|
maxExtensionsReached:
|
||||||
|
'拡張機能数が上限({{max}}個)に達しました。新しい MCP サーバーやプラグインを追加するには、既存のものを削除してください。',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default jaJP;
|
export default jaJP;
|
||||||
|
|||||||
@@ -922,6 +922,14 @@ const zhHans = {
|
|||||||
sessions: '会话记录',
|
sessions: '会话记录',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
limitation: {
|
||||||
|
maxBotsReached:
|
||||||
|
'已达到机器人数量上限({{max}}个)。请先删除已有机器人后再创建新的。',
|
||||||
|
maxPipelinesReached:
|
||||||
|
'已达到流水线数量上限({{max}}个)。请先删除已有流水线后再创建新的。',
|
||||||
|
maxExtensionsReached:
|
||||||
|
'已达到扩展数量上限({{max}}个)。请先删除已有的 MCP 服务器或插件后再添加新的。',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default zhHans;
|
export default zhHans;
|
||||||
|
|||||||
@@ -897,6 +897,14 @@ const zhHant = {
|
|||||||
sessions: '會話記錄',
|
sessions: '會話記錄',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
limitation: {
|
||||||
|
maxBotsReached:
|
||||||
|
'已達到機器人數量上限({{max}}個)。請先刪除已有機器人後再建立新的。',
|
||||||
|
maxPipelinesReached:
|
||||||
|
'已達到流水線數量上限({{max}}個)。請先刪除已有流水線後再建立新的。',
|
||||||
|
maxExtensionsReached:
|
||||||
|
'已達到擴充功能數量上限({{max}}個)。請先刪除已有的 MCP 伺服器或外掛後再新增。',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default zhHant;
|
export default zhHant;
|
||||||
|
|||||||
Reference in New Issue
Block a user