From 87131cf03b98b2c541b07aa25bdb3e3debacea4e Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Thu, 27 Nov 2025 11:52:15 +0800 Subject: [PATCH] Feat/pipeline enable all extensions (#1807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 添加流水线扩展集成"启用所有"选项 为流水线的扩展集成配置添加独立的"启用所有插件"和"启用所有MCP服务器"选项。 主要变更: - 数据模型:在 extensions_preferences 中添加 enable_all_plugins 和 enable_all_mcp_servers 字段 - 后端逻辑:修改 RuntimePipeline 以支持独立的启用所有选项,当启用时设置为 None 表示使用所有可用资源 - API 接口:更新 GET/PUT /api/v1/pipelines/{uuid}/extensions 以支持新字段 - 前端 UI:为插件和 MCP 服务器分别添加独立的开关控件 - 国际化:添加对应的中文翻译文本 - 默认行为:新创建的流水线默认启用所有插件和 MCP 服务器 🤖 Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy * fix(i18n): add missing translations for pipeline extensions Added translations for enable all plugins/MCP servers feature: - en-US: English translations - ja-JP: Japanese translations - zh-Hant: Traditional Chinese translations Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy * chore: add migration for enable all extensions config * fix: bad renaming --------- Co-authored-by: Claude Co-authored-by: Happy --- .../controller/groups/pipelines/pipelines.py | 11 ++- src/langbot/pkg/api/http/service/pipeline.py | 18 +++- .../pkg/entity/persistence/pipeline.py | 6 +- .../dbm011_dify_base_prompt_config.py | 2 +- .../dbm012_pipeline_extensions_enable_all.py | 47 +++++++++ src/langbot/pkg/pipeline/pipelinemgr.py | 33 +++++-- src/langbot/pkg/utils/constants.py | 2 +- .../pipeline-extensions/PipelineExtension.tsx | 96 +++++++++++++++++-- web/src/app/infra/http/BackendClient.ts | 6 ++ web/src/i18n/locales/en-US.ts | 4 + web/src/i18n/locales/ja-JP.ts | 4 + web/src/i18n/locales/zh-Hans.ts | 4 + web/src/i18n/locales/zh-Hant.ts | 4 + 13 files changed, 213 insertions(+), 24 deletions(-) create mode 100644 src/langbot/pkg/persistence/migrations/dbm012_pipeline_extensions_enable_all.py diff --git a/src/langbot/pkg/api/http/controller/groups/pipelines/pipelines.py b/src/langbot/pkg/api/http/controller/groups/pipelines/pipelines.py index 924b68d4..0e80d0d4 100644 --- a/src/langbot/pkg/api/http/controller/groups/pipelines/pipelines.py +++ b/src/langbot/pkg/api/http/controller/groups/pipelines/pipelines.py @@ -62,22 +62,27 @@ class PipelinesRouterGroup(group.RouterGroup): plugins = await self.ap.plugin_connector.list_plugins() mcp_servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True) + extensions_prefs = pipeline.get('extensions_preferences', {}) return self.success( data={ - 'bound_plugins': pipeline.get('extensions_preferences', {}).get('plugins', []), + 'enable_all_plugins': extensions_prefs.get('enable_all_plugins', True), + 'enable_all_mcp_servers': extensions_prefs.get('enable_all_mcp_servers', True), + 'bound_plugins': extensions_prefs.get('plugins', []), 'available_plugins': plugins, - 'bound_mcp_servers': pipeline.get('extensions_preferences', {}).get('mcp_servers', []), + 'bound_mcp_servers': extensions_prefs.get('mcp_servers', []), 'available_mcp_servers': mcp_servers, } ) elif quart.request.method == 'PUT': # Update bound plugins and MCP servers for this pipeline json_data = await quart.request.json + enable_all_plugins = json_data.get('enable_all_plugins', True) + enable_all_mcp_servers = json_data.get('enable_all_mcp_servers', True) bound_plugins = json_data.get('bound_plugins', []) bound_mcp_servers = json_data.get('bound_mcp_servers', []) await self.ap.pipeline_service.update_pipeline_extensions( - pipeline_uuid, bound_plugins, bound_mcp_servers + pipeline_uuid, bound_plugins, bound_mcp_servers, enable_all_plugins, enable_all_mcp_servers ) return self.success() diff --git a/src/langbot/pkg/api/http/service/pipeline.py b/src/langbot/pkg/api/http/service/pipeline.py index 62e0879f..e3f2770c 100644 --- a/src/langbot/pkg/api/http/service/pipeline.py +++ b/src/langbot/pkg/api/http/service/pipeline.py @@ -85,6 +85,15 @@ class PipelineService: with open(template_path, 'r', encoding='utf-8') as f: pipeline_data['config'] = json.load(f) + # Ensure extensions_preferences is set with enable_all_plugins and enable_all_mcp_servers=True by default + if 'extensions_preferences' not in pipeline_data: + pipeline_data['extensions_preferences'] = { + 'enable_all_plugins': True, + 'enable_all_mcp_servers': True, + 'plugins': [], + 'mcp_servers': [], + } + await self.ap.persistence_mgr.execute_async( sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data) ) @@ -143,7 +152,12 @@ class PipelineService: await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid) async def update_pipeline_extensions( - self, pipeline_uuid: str, bound_plugins: list[dict], bound_mcp_servers: list[str] = None + self, + pipeline_uuid: str, + bound_plugins: list[dict], + bound_mcp_servers: list[str] = None, + enable_all_plugins: bool = True, + enable_all_mcp_servers: bool = True, ) -> None: """Update the bound plugins and MCP servers for a pipeline""" # Get current pipeline @@ -159,6 +173,8 @@ class PipelineService: # Update extensions_preferences extensions_preferences = pipeline.extensions_preferences or {} + extensions_preferences['enable_all_plugins'] = enable_all_plugins + extensions_preferences['enable_all_mcp_servers'] = enable_all_mcp_servers extensions_preferences['plugins'] = bound_plugins if bound_mcp_servers is not None: extensions_preferences['mcp_servers'] = bound_mcp_servers diff --git a/src/langbot/pkg/entity/persistence/pipeline.py b/src/langbot/pkg/entity/persistence/pipeline.py index 8c0093aa..5ef2956d 100644 --- a/src/langbot/pkg/entity/persistence/pipeline.py +++ b/src/langbot/pkg/entity/persistence/pipeline.py @@ -22,7 +22,11 @@ class LegacyPipeline(Base): is_default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) stages = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) - extensions_preferences = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={}) + extensions_preferences = sqlalchemy.Column( + sqlalchemy.JSON, + nullable=False, + default={'enable_all_plugins': True, 'enable_all_mcp_servers': True, 'plugins': [], 'mcp_servers': []}, + ) class PipelineRunRecord(Base): diff --git a/src/langbot/pkg/persistence/migrations/dbm011_dify_base_prompt_config.py b/src/langbot/pkg/persistence/migrations/dbm011_dify_base_prompt_config.py index a98c8050..e4a5cedf 100644 --- a/src/langbot/pkg/persistence/migrations/dbm011_dify_base_prompt_config.py +++ b/src/langbot/pkg/persistence/migrations/dbm011_dify_base_prompt_config.py @@ -7,7 +7,7 @@ from ...entity.persistence import pipeline as persistence_pipeline @migration.migration_class(11) class DBMigrateDifyApiConfig(migration.DBMigration): - """Langflow API config""" + """Dify base prompt config""" async def upgrade(self): """Upgrade""" diff --git a/src/langbot/pkg/persistence/migrations/dbm012_pipeline_extensions_enable_all.py b/src/langbot/pkg/persistence/migrations/dbm012_pipeline_extensions_enable_all.py new file mode 100644 index 00000000..10b6e465 --- /dev/null +++ b/src/langbot/pkg/persistence/migrations/dbm012_pipeline_extensions_enable_all.py @@ -0,0 +1,47 @@ +from .. import migration + +import sqlalchemy + +from ...entity.persistence import pipeline as persistence_pipeline + + +@migration.migration_class(12) +class DBMigratePipelineExtensionsEnableAll(migration.DBMigration): + """Pipeline extensions enable all""" + + async def upgrade(self): + """Upgrade""" + # read all pipelines + pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline)) + + for pipeline in pipelines: + serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) + + extensions_preferences = serialized_pipeline['extensions_preferences'] + + if 'enable_all_plugins' not in extensions_preferences: + if 'plugins' in extensions_preferences: + extensions_preferences['enable_all_plugins'] = False + else: + extensions_preferences['enable_all_plugins'] = True + extensions_preferences['plugins'] = [] + + if 'enable_all_mcp_servers' not in extensions_preferences: + if 'mcp_servers' in extensions_preferences: + extensions_preferences['enable_all_mcp_servers'] = False + else: + extensions_preferences['enable_all_mcp_servers'] = True + extensions_preferences['mcp_servers'] = [] + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.update(persistence_pipeline.LegacyPipeline) + .where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid']) + .values( + extensions_preferences=extensions_preferences, + for_version=self.ap.ver_mgr.get_current_version(), + ) + ) + + async def downgrade(self): + """Downgrade""" + pass diff --git a/src/langbot/pkg/pipeline/pipelinemgr.py b/src/langbot/pkg/pipeline/pipelinemgr.py index 9470eb23..d9915583 100644 --- a/src/langbot/pkg/pipeline/pipelinemgr.py +++ b/src/langbot/pkg/pipeline/pipelinemgr.py @@ -69,11 +69,17 @@ class RuntimePipeline: stage_containers: list[StageInstContainer] """阶段实例容器""" - bound_plugins: list[str] - """绑定到此流水线的插件列表(格式:author/plugin_name)""" + bound_plugins: list[str] | None + """绑定到此流水线的插件列表(格式:author/plugin_name),None表示启用所有""" - bound_mcp_servers: list[str] - """绑定到此流水线的MCP服务器列表(格式:uuid)""" + bound_mcp_servers: list[str] | None + """绑定到此流水线的MCP服务器列表(格式:uuid),None表示启用所有""" + + enable_all_plugins: bool + """是否启用所有插件""" + + enable_all_mcp_servers: bool + """是否启用所有MCP服务器""" def __init__( self, @@ -87,11 +93,22 @@ class RuntimePipeline: # Extract bound plugins and MCP servers from extensions_preferences extensions_prefs = pipeline_entity.extensions_preferences or {} - plugin_list = extensions_prefs.get('plugins', []) - self.bound_plugins = [f'{p["author"]}/{p["name"]}' for p in plugin_list] if plugin_list else [] + self.enable_all_plugins = extensions_prefs.get('enable_all_plugins', True) + self.enable_all_mcp_servers = extensions_prefs.get('enable_all_mcp_servers', True) - mcp_server_list = extensions_prefs.get('mcp_servers', []) - self.bound_mcp_servers = mcp_server_list if mcp_server_list else [] + if self.enable_all_plugins: + # None indicates to use all available plugins + self.bound_plugins = None + else: + plugin_list = extensions_prefs.get('plugins', []) + self.bound_plugins = [f'{p["author"]}/{p["name"]}' for p in plugin_list] if plugin_list else [] + + if self.enable_all_mcp_servers: + # None indicates to use all available MCP servers + self.bound_mcp_servers = None + else: + mcp_server_list = extensions_prefs.get('mcp_servers', []) + self.bound_mcp_servers = mcp_server_list if mcp_server_list else [] async def run(self, query: pipeline_query.Query): query.pipeline_config = self.pipeline_entity.config diff --git a/src/langbot/pkg/utils/constants.py b/src/langbot/pkg/utils/constants.py index dba385cd..5f61bb79 100644 --- a/src/langbot/pkg/utils/constants.py +++ b/src/langbot/pkg/utils/constants.py @@ -2,7 +2,7 @@ import langbot semantic_version = f'v{langbot.__version__}' -required_database_version = 11 +required_database_version = 12 """Tag the version of the database schema, used to check if the database needs to be migrated""" debug_mode = False diff --git a/web/src/app/home/pipelines/components/pipeline-extensions/PipelineExtension.tsx b/web/src/app/home/pipelines/components/pipeline-extensions/PipelineExtension.tsx index 947662ed..2f411f4e 100644 --- a/web/src/app/home/pipelines/components/pipeline-extensions/PipelineExtension.tsx +++ b/web/src/app/home/pipelines/components/pipeline-extensions/PipelineExtension.tsx @@ -16,6 +16,8 @@ import { import { Checkbox } from '@/components/ui/checkbox'; import { Plus, X, Server, Wrench } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; import { Plugin } from '@/app/infra/entities/plugin'; import { MCPServer } from '@/app/infra/entities/api'; import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList'; @@ -27,6 +29,8 @@ export default function PipelineExtension({ }) { const { t } = useTranslation(); const [loading, setLoading] = useState(true); + const [enableAllPlugins, setEnableAllPlugins] = useState(true); + const [enableAllMCPServers, setEnableAllMCPServers] = useState(true); const [selectedPlugins, setSelectedPlugins] = useState([]); const [allPlugins, setAllPlugins] = useState([]); const [selectedMCPServers, setSelectedMCPServers] = useState([]); @@ -53,6 +57,9 @@ export default function PipelineExtension({ setLoading(true); const data = await backendClient.getPipelineExtensions(pipelineId); + setEnableAllPlugins(data.enable_all_plugins ?? true); + setEnableAllMCPServers(data.enable_all_mcp_servers ?? true); + const boundPluginIds = new Set( data.bound_plugins.map((p) => `${p.author}/${p.name}`), ); @@ -80,7 +87,12 @@ export default function PipelineExtension({ } }; - const saveToBackend = async (plugins: Plugin[], mcpServers: MCPServer[]) => { + const saveToBackend = async ( + plugins: Plugin[], + mcpServers: MCPServer[], + newEnableAllPlugins?: boolean, + newEnableAllMCPServers?: boolean, + ) => { try { const boundPluginsArray = plugins.map((plugin) => { const metadata = plugin.manifest.manifest.metadata; @@ -96,6 +108,8 @@ export default function PipelineExtension({ pipelineId, boundPluginsArray, boundMCPServerIds, + newEnableAllPlugins ?? enableAllPlugins, + newEnableAllMCPServers ?? enableAllMCPServers, ); toast.success(t('pipelines.extensions.saveSuccess')); } catch (error) { @@ -184,6 +198,26 @@ export default function PipelineExtension({ await saveToBackend(selectedPlugins, newSelected); }; + const handleToggleEnableAllPlugins = async (checked: boolean) => { + setEnableAllPlugins(checked); + await saveToBackend( + selectedPlugins, + selectedMCPServers, + checked, + undefined, + ); + }; + + const handleToggleEnableAllMCPServers = async (checked: boolean) => { + setEnableAllMCPServers(checked); + await saveToBackend( + selectedPlugins, + selectedMCPServers, + undefined, + checked, + ); + }; + if (loading) { return (
@@ -198,11 +232,32 @@ export default function PipelineExtension({
{/* Plugins Section */}
-

- {t('pipelines.extensions.pluginsTitle')} -

+
+

+ {t('pipelines.extensions.pluginsTitle')} +

+
+ + +
+
- {selectedPlugins.length === 0 ? ( + {enableAllPlugins ? ( +
+

+ {t('pipelines.extensions.allPluginsEnabled')} +

+
+ ) : selectedPlugins.length === 0 ? (

{t('pipelines.extensions.noPluginsSelected')} @@ -278,6 +333,7 @@ export default function PipelineExtension({ onClick={handleOpenPluginDialog} variant="outline" className="w-full" + disabled={enableAllPlugins} > {t('pipelines.extensions.addPlugin')} @@ -286,11 +342,32 @@ export default function PipelineExtension({ {/* MCP Servers Section */}

-

- {t('pipelines.extensions.mcpServersTitle')} -

+
+

+ {t('pipelines.extensions.mcpServersTitle')} +

+
+ + +
+
- {selectedMCPServers.length === 0 ? ( + {enableAllMCPServers ? ( +
+

+ {t('pipelines.extensions.allMCPServersEnabled')} +

+
+ ) : selectedMCPServers.length === 0 ? (

{t('pipelines.extensions.noMCPServersSelected')} @@ -350,6 +427,7 @@ export default function PipelineExtension({ onClick={handleOpenMCPDialog} variant="outline" className="w-full" + disabled={enableAllMCPServers} > {t('pipelines.extensions.addMCPServer')} diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 4a8512e1..3a8c8a53 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -171,6 +171,8 @@ export class BackendClient extends BaseHttpClient { } public getPipelineExtensions(uuid: string): Promise<{ + enable_all_plugins: boolean; + enable_all_mcp_servers: boolean; bound_plugins: Array<{ author: string; name: string }>; available_plugins: Plugin[]; bound_mcp_servers: string[]; @@ -183,10 +185,14 @@ export class BackendClient extends BaseHttpClient { uuid: string, bound_plugins: Array<{ author: string; name: string }>, bound_mcp_servers: string[], + enable_all_plugins: boolean = true, + enable_all_mcp_servers: boolean = true, ): Promise { return this.put(`/api/v1/pipelines/${uuid}/extensions`, { bound_plugins, bound_mcp_servers, + enable_all_plugins, + enable_all_mcp_servers, }); } diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index b08a887a..6c6d8659 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -494,6 +494,10 @@ const enUS = { noPluginsInstalled: 'No installed plugins', noMCPServersConfigured: 'No configured MCP servers', selectAll: 'Select All', + enableAllPlugins: 'Enable All Plugins', + enableAllMCPServers: 'Enable All MCP Servers', + allPluginsEnabled: 'All plugins enabled', + allMCPServersEnabled: 'All MCP servers enabled', }, debugDialog: { title: 'Pipeline Chat', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index c7cf6a3b..54d5b275 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -497,6 +497,10 @@ const jaJP = { noPluginsInstalled: 'インストールされているプラグインがありません', noMCPServersConfigured: '設定されているMCPサーバーがありません', selectAll: 'すべて選択', + enableAllPlugins: 'すべてのプラグインを有効にする', + enableAllMCPServers: 'すべてのMCPサーバーを有効にする', + allPluginsEnabled: 'すべてのプラグインが有効になっています', + allMCPServersEnabled: 'すべてのMCPサーバーが有効になっています', }, debugDialog: { title: 'パイプラインのチャット', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 5d11ef87..bde82cdd 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -476,6 +476,10 @@ const zhHans = { noPluginsInstalled: '无已安装的插件', noMCPServersConfigured: '无已配置的 MCP 服务器', selectAll: '全选', + enableAllPlugins: '启用所有插件', + enableAllMCPServers: '启用所有 MCP 服务器', + allPluginsEnabled: '已启用所有插件', + allMCPServersEnabled: '已启用所有 MCP 服务器', }, debugDialog: { title: '流水线对话', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index ecfb9059..ceb7fab1 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -474,6 +474,10 @@ const zhHant = { noPluginsInstalled: '無已安裝的插件', noMCPServersConfigured: '無已配置的 MCP 伺服器', selectAll: '全選', + enableAllPlugins: '啟用所有插件', + enableAllMCPServers: '啟用所有 MCP 伺服器', + allPluginsEnabled: '已啟用所有插件', + allMCPServersEnabled: '已啟用所有 MCP 伺服器', }, debugDialog: { title: '流程線對話',