Feat/pipeline enable all extensions (#1807)

* 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 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* 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 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* chore: add migration for enable all extensions config

* fix: bad renaming

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
This commit is contained in:
Junyan Qin (Chin)
2025-11-27 11:52:15 +08:00
committed by GitHub
parent 7d51293594
commit 87131cf03b
13 changed files with 213 additions and 24 deletions

View File

@@ -62,22 +62,27 @@ class PipelinesRouterGroup(group.RouterGroup):
plugins = await self.ap.plugin_connector.list_plugins() plugins = await self.ap.plugin_connector.list_plugins()
mcp_servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True) mcp_servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True)
extensions_prefs = pipeline.get('extensions_preferences', {})
return self.success( return self.success(
data={ 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, '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, 'available_mcp_servers': mcp_servers,
} }
) )
elif quart.request.method == 'PUT': elif quart.request.method == 'PUT':
# Update bound plugins and MCP servers for this pipeline # Update bound plugins and MCP servers for this pipeline
json_data = await quart.request.json 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_plugins = json_data.get('bound_plugins', [])
bound_mcp_servers = json_data.get('bound_mcp_servers', []) bound_mcp_servers = json_data.get('bound_mcp_servers', [])
await self.ap.pipeline_service.update_pipeline_extensions( 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() return self.success()

View File

@@ -85,6 +85,15 @@ class PipelineService:
with open(template_path, 'r', encoding='utf-8') as f: with open(template_path, 'r', encoding='utf-8') as f:
pipeline_data['config'] = json.load(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( await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data) sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data)
) )
@@ -143,7 +152,12 @@ class PipelineService:
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid) await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
async def update_pipeline_extensions( 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: ) -> None:
"""Update the bound plugins and MCP servers for a pipeline""" """Update the bound plugins and MCP servers for a pipeline"""
# Get current pipeline # Get current pipeline
@@ -159,6 +173,8 @@ class PipelineService:
# Update extensions_preferences # Update extensions_preferences
extensions_preferences = pipeline.extensions_preferences or {} 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 extensions_preferences['plugins'] = bound_plugins
if bound_mcp_servers is not None: if bound_mcp_servers is not None:
extensions_preferences['mcp_servers'] = bound_mcp_servers extensions_preferences['mcp_servers'] = bound_mcp_servers

View File

@@ -22,7 +22,11 @@ class LegacyPipeline(Base):
is_default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) is_default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False)
stages = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) stages = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
config = 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): class PipelineRunRecord(Base):

View File

@@ -7,7 +7,7 @@ from ...entity.persistence import pipeline as persistence_pipeline
@migration.migration_class(11) @migration.migration_class(11)
class DBMigrateDifyApiConfig(migration.DBMigration): class DBMigrateDifyApiConfig(migration.DBMigration):
"""Langflow API config""" """Dify base prompt config"""
async def upgrade(self): async def upgrade(self):
"""Upgrade""" """Upgrade"""

View File

@@ -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

View File

@@ -69,11 +69,17 @@ class RuntimePipeline:
stage_containers: list[StageInstContainer] stage_containers: list[StageInstContainer]
"""阶段实例容器""" """阶段实例容器"""
bound_plugins: list[str] bound_plugins: list[str] | None
"""绑定到此流水线的插件列表格式author/plugin_name""" """绑定到此流水线的插件列表格式author/plugin_nameNone表示启用所有"""
bound_mcp_servers: list[str] bound_mcp_servers: list[str] | None
"""绑定到此流水线的MCP服务器列表格式uuid""" """绑定到此流水线的MCP服务器列表格式uuidNone表示启用所有"""
enable_all_plugins: bool
"""是否启用所有插件"""
enable_all_mcp_servers: bool
"""是否启用所有MCP服务器"""
def __init__( def __init__(
self, self,
@@ -87,11 +93,22 @@ class RuntimePipeline:
# Extract bound plugins and MCP servers from extensions_preferences # Extract bound plugins and MCP servers from extensions_preferences
extensions_prefs = pipeline_entity.extensions_preferences or {} extensions_prefs = pipeline_entity.extensions_preferences or {}
plugin_list = extensions_prefs.get('plugins', []) self.enable_all_plugins = extensions_prefs.get('enable_all_plugins', True)
self.bound_plugins = [f'{p["author"]}/{p["name"]}' for p in plugin_list] if plugin_list else [] self.enable_all_mcp_servers = extensions_prefs.get('enable_all_mcp_servers', True)
mcp_server_list = extensions_prefs.get('mcp_servers', []) if self.enable_all_plugins:
self.bound_mcp_servers = mcp_server_list if mcp_server_list else [] # 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): async def run(self, query: pipeline_query.Query):
query.pipeline_config = self.pipeline_entity.config query.pipeline_config = self.pipeline_entity.config

View File

@@ -2,7 +2,7 @@ import langbot
semantic_version = f'v{langbot.__version__}' 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""" """Tag the version of the database schema, used to check if the database needs to be migrated"""
debug_mode = False debug_mode = False

View File

@@ -16,6 +16,8 @@ import {
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Plus, X, Server, Wrench } from 'lucide-react'; import { Plus, X, Server, Wrench } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; 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 { Plugin } from '@/app/infra/entities/plugin';
import { MCPServer } from '@/app/infra/entities/api'; import { MCPServer } from '@/app/infra/entities/api';
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList'; import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
@@ -27,6 +29,8 @@ export default function PipelineExtension({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [enableAllPlugins, setEnableAllPlugins] = useState(true);
const [enableAllMCPServers, setEnableAllMCPServers] = useState(true);
const [selectedPlugins, setSelectedPlugins] = useState<Plugin[]>([]); const [selectedPlugins, setSelectedPlugins] = useState<Plugin[]>([]);
const [allPlugins, setAllPlugins] = useState<Plugin[]>([]); const [allPlugins, setAllPlugins] = useState<Plugin[]>([]);
const [selectedMCPServers, setSelectedMCPServers] = useState<MCPServer[]>([]); const [selectedMCPServers, setSelectedMCPServers] = useState<MCPServer[]>([]);
@@ -53,6 +57,9 @@ export default function PipelineExtension({
setLoading(true); setLoading(true);
const data = await backendClient.getPipelineExtensions(pipelineId); const data = await backendClient.getPipelineExtensions(pipelineId);
setEnableAllPlugins(data.enable_all_plugins ?? true);
setEnableAllMCPServers(data.enable_all_mcp_servers ?? true);
const boundPluginIds = new Set( const boundPluginIds = new Set(
data.bound_plugins.map((p) => `${p.author}/${p.name}`), 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 { try {
const boundPluginsArray = plugins.map((plugin) => { const boundPluginsArray = plugins.map((plugin) => {
const metadata = plugin.manifest.manifest.metadata; const metadata = plugin.manifest.manifest.metadata;
@@ -96,6 +108,8 @@ export default function PipelineExtension({
pipelineId, pipelineId,
boundPluginsArray, boundPluginsArray,
boundMCPServerIds, boundMCPServerIds,
newEnableAllPlugins ?? enableAllPlugins,
newEnableAllMCPServers ?? enableAllMCPServers,
); );
toast.success(t('pipelines.extensions.saveSuccess')); toast.success(t('pipelines.extensions.saveSuccess'));
} catch (error) { } catch (error) {
@@ -184,6 +198,26 @@ export default function PipelineExtension({
await saveToBackend(selectedPlugins, newSelected); 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) { if (loading) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -198,11 +232,32 @@ export default function PipelineExtension({
<div className="space-y-6"> <div className="space-y-6">
{/* Plugins Section */} {/* Plugins Section */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-semibold text-foreground"> <div className="flex items-center justify-between">
{t('pipelines.extensions.pluginsTitle')} <h3 className="text-sm font-semibold text-foreground">
</h3> {t('pipelines.extensions.pluginsTitle')}
</h3>
<div className="flex items-center gap-2">
<Label
htmlFor="enable-all-plugins"
className="text-sm font-normal cursor-pointer"
>
{t('pipelines.extensions.enableAllPlugins')}
</Label>
<Switch
id="enable-all-plugins"
checked={enableAllPlugins}
onCheckedChange={handleToggleEnableAllPlugins}
/>
</div>
</div>
<div className="space-y-2"> <div className="space-y-2">
{selectedPlugins.length === 0 ? ( {enableAllPlugins ? (
<div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/30">
<p className="text-sm text-muted-foreground">
{t('pipelines.extensions.allPluginsEnabled')}
</p>
</div>
) : selectedPlugins.length === 0 ? (
<div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border"> <div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t('pipelines.extensions.noPluginsSelected')} {t('pipelines.extensions.noPluginsSelected')}
@@ -278,6 +333,7 @@ export default function PipelineExtension({
onClick={handleOpenPluginDialog} onClick={handleOpenPluginDialog}
variant="outline" variant="outline"
className="w-full" className="w-full"
disabled={enableAllPlugins}
> >
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
{t('pipelines.extensions.addPlugin')} {t('pipelines.extensions.addPlugin')}
@@ -286,11 +342,32 @@ export default function PipelineExtension({
{/* MCP Servers Section */} {/* MCP Servers Section */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-semibold text-foreground"> <div className="flex items-center justify-between">
{t('pipelines.extensions.mcpServersTitle')} <h3 className="text-sm font-semibold text-foreground">
</h3> {t('pipelines.extensions.mcpServersTitle')}
</h3>
<div className="flex items-center gap-2">
<Label
htmlFor="enable-all-mcp-servers"
className="text-sm font-normal cursor-pointer"
>
{t('pipelines.extensions.enableAllMCPServers')}
</Label>
<Switch
id="enable-all-mcp-servers"
checked={enableAllMCPServers}
onCheckedChange={handleToggleEnableAllMCPServers}
/>
</div>
</div>
<div className="space-y-2"> <div className="space-y-2">
{selectedMCPServers.length === 0 ? ( {enableAllMCPServers ? (
<div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/30">
<p className="text-sm text-muted-foreground">
{t('pipelines.extensions.allMCPServersEnabled')}
</p>
</div>
) : selectedMCPServers.length === 0 ? (
<div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border"> <div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t('pipelines.extensions.noMCPServersSelected')} {t('pipelines.extensions.noMCPServersSelected')}
@@ -350,6 +427,7 @@ export default function PipelineExtension({
onClick={handleOpenMCPDialog} onClick={handleOpenMCPDialog}
variant="outline" variant="outline"
className="w-full" className="w-full"
disabled={enableAllMCPServers}
> >
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
{t('pipelines.extensions.addMCPServer')} {t('pipelines.extensions.addMCPServer')}

View File

@@ -171,6 +171,8 @@ export class BackendClient extends BaseHttpClient {
} }
public getPipelineExtensions(uuid: string): Promise<{ public getPipelineExtensions(uuid: string): Promise<{
enable_all_plugins: boolean;
enable_all_mcp_servers: boolean;
bound_plugins: Array<{ author: string; name: string }>; bound_plugins: Array<{ author: string; name: string }>;
available_plugins: Plugin[]; available_plugins: Plugin[];
bound_mcp_servers: string[]; bound_mcp_servers: string[];
@@ -183,10 +185,14 @@ export class BackendClient extends BaseHttpClient {
uuid: string, uuid: string,
bound_plugins: Array<{ author: string; name: string }>, bound_plugins: Array<{ author: string; name: string }>,
bound_mcp_servers: string[], bound_mcp_servers: string[],
enable_all_plugins: boolean = true,
enable_all_mcp_servers: boolean = true,
): Promise<object> { ): Promise<object> {
return this.put(`/api/v1/pipelines/${uuid}/extensions`, { return this.put(`/api/v1/pipelines/${uuid}/extensions`, {
bound_plugins, bound_plugins,
bound_mcp_servers, bound_mcp_servers,
enable_all_plugins,
enable_all_mcp_servers,
}); });
} }

View File

@@ -494,6 +494,10 @@ const enUS = {
noPluginsInstalled: 'No installed plugins', noPluginsInstalled: 'No installed plugins',
noMCPServersConfigured: 'No configured MCP servers', noMCPServersConfigured: 'No configured MCP servers',
selectAll: 'Select All', selectAll: 'Select All',
enableAllPlugins: 'Enable All Plugins',
enableAllMCPServers: 'Enable All MCP Servers',
allPluginsEnabled: 'All plugins enabled',
allMCPServersEnabled: 'All MCP servers enabled',
}, },
debugDialog: { debugDialog: {
title: 'Pipeline Chat', title: 'Pipeline Chat',

View File

@@ -497,6 +497,10 @@ const jaJP = {
noPluginsInstalled: 'インストールされているプラグインがありません', noPluginsInstalled: 'インストールされているプラグインがありません',
noMCPServersConfigured: '設定されているMCPサーバーがありません', noMCPServersConfigured: '設定されているMCPサーバーがありません',
selectAll: 'すべて選択', selectAll: 'すべて選択',
enableAllPlugins: 'すべてのプラグインを有効にする',
enableAllMCPServers: 'すべてのMCPサーバーを有効にする',
allPluginsEnabled: 'すべてのプラグインが有効になっています',
allMCPServersEnabled: 'すべてのMCPサーバーが有効になっています',
}, },
debugDialog: { debugDialog: {
title: 'パイプラインのチャット', title: 'パイプラインのチャット',

View File

@@ -476,6 +476,10 @@ const zhHans = {
noPluginsInstalled: '无已安装的插件', noPluginsInstalled: '无已安装的插件',
noMCPServersConfigured: '无已配置的 MCP 服务器', noMCPServersConfigured: '无已配置的 MCP 服务器',
selectAll: '全选', selectAll: '全选',
enableAllPlugins: '启用所有插件',
enableAllMCPServers: '启用所有 MCP 服务器',
allPluginsEnabled: '已启用所有插件',
allMCPServersEnabled: '已启用所有 MCP 服务器',
}, },
debugDialog: { debugDialog: {
title: '流水线对话', title: '流水线对话',

View File

@@ -474,6 +474,10 @@ const zhHant = {
noPluginsInstalled: '無已安裝的插件', noPluginsInstalled: '無已安裝的插件',
noMCPServersConfigured: '無已配置的 MCP 伺服器', noMCPServersConfigured: '無已配置的 MCP 伺服器',
selectAll: '全選', selectAll: '全選',
enableAllPlugins: '啟用所有插件',
enableAllMCPServers: '啟用所有 MCP 伺服器',
allPluginsEnabled: '已啟用所有插件',
allMCPServersEnabled: '已啟用所有 MCP 伺服器',
}, },
debugDialog: { debugDialog: {
title: '流程線對話', title: '流程線對話',