mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-26 03:44:58 +08:00
feat: add MCP server selection to pipeline extensions (#1754)
* Initial plan * Backend: Add MCP server selection support to pipeline extensions Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Frontend: Add MCP server selection UI to pipeline extensions Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * perf: ui * perf: ui * perf: desc for extension page --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -56,18 +56,24 @@ class PipelinesRouterGroup(group.RouterGroup):
|
|||||||
return self.http_status(404, -1, 'pipeline not found')
|
return self.http_status(404, -1, 'pipeline not found')
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
return self.success(
|
return self.success(
|
||||||
data={
|
data={
|
||||||
'bound_plugins': pipeline.get('extensions_preferences', {}).get('plugins', []),
|
'bound_plugins': pipeline.get('extensions_preferences', {}).get('plugins', []),
|
||||||
'available_plugins': plugins,
|
'available_plugins': plugins,
|
||||||
|
'bound_mcp_servers': pipeline.get('extensions_preferences', {}).get('mcp_servers', []),
|
||||||
|
'available_mcp_servers': mcp_servers,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif quart.request.method == 'PUT':
|
elif quart.request.method == 'PUT':
|
||||||
# Update bound plugins for this pipeline
|
# Update bound plugins and MCP servers for this pipeline
|
||||||
json_data = await quart.request.json
|
json_data = await quart.request.json
|
||||||
bound_plugins = json_data.get('bound_plugins', [])
|
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)
|
await self.ap.pipeline_service.update_pipeline_extensions(
|
||||||
|
pipeline_uuid, bound_plugins, bound_mcp_servers
|
||||||
|
)
|
||||||
|
|
||||||
return self.success()
|
return self.success()
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ 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(self, pipeline_uuid: str, bound_plugins: list[dict]) -> None:
|
async def update_pipeline_extensions(self, pipeline_uuid: str, bound_plugins: list[dict], bound_mcp_servers: list[str] = None) -> None:
|
||||||
"""Update the bound plugins for a pipeline"""
|
"""Update the bound plugins and MCP servers for a pipeline"""
|
||||||
# Get current pipeline
|
# Get current 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(
|
||||||
@@ -153,6 +153,8 @@ class PipelineService:
|
|||||||
# Update extensions_preferences
|
# Update extensions_preferences
|
||||||
extensions_preferences = pipeline.extensions_preferences or {}
|
extensions_preferences = pipeline.extensions_preferences or {}
|
||||||
extensions_preferences['plugins'] = bound_plugins
|
extensions_preferences['plugins'] = bound_plugins
|
||||||
|
if bound_mcp_servers is not None:
|
||||||
|
extensions_preferences['mcp_servers'] = bound_mcp_servers
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ class RuntimePipeline:
|
|||||||
|
|
||||||
bound_plugins: list[str]
|
bound_plugins: list[str]
|
||||||
"""绑定到此流水线的插件列表(格式:author/plugin_name)"""
|
"""绑定到此流水线的插件列表(格式:author/plugin_name)"""
|
||||||
|
|
||||||
|
bound_mcp_servers: list[str]
|
||||||
|
"""绑定到此流水线的MCP服务器列表(格式:uuid)"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -82,15 +85,19 @@ class RuntimePipeline:
|
|||||||
self.pipeline_entity = pipeline_entity
|
self.pipeline_entity = pipeline_entity
|
||||||
self.stage_containers = stage_containers
|
self.stage_containers = stage_containers
|
||||||
|
|
||||||
# Extract bound plugins 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', [])
|
plugin_list = extensions_prefs.get('plugins', [])
|
||||||
self.bound_plugins = [f"{p['author']}/{p['name']}" for p in plugin_list] if plugin_list else []
|
self.bound_plugins = [f"{p['author']}/{p['name']}" for p in plugin_list] if plugin_list 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
|
||||||
# Store bound plugins in query for filtering
|
# Store bound plugins and MCP servers in query for filtering
|
||||||
query.variables['_pipeline_bound_plugins'] = self.bound_plugins
|
query.variables['_pipeline_bound_plugins'] = self.bound_plugins
|
||||||
|
query.variables['_pipeline_bound_mcp_servers'] = self.bound_mcp_servers
|
||||||
await self.process_query(query)
|
await self.process_query(query)
|
||||||
|
|
||||||
async def _check_output(self, query: pipeline_query.Query, result: pipeline_entities.StageProcessResult):
|
async def _check_output(self, query: pipeline_query.Query, result: pipeline_entities.StageProcessResult):
|
||||||
|
|||||||
@@ -65,9 +65,14 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
query.use_llm_model_uuid = llm_model.model_entity.uuid
|
query.use_llm_model_uuid = llm_model.model_entity.uuid
|
||||||
|
|
||||||
if llm_model.model_entity.abilities.__contains__('func_call'):
|
if llm_model.model_entity.abilities.__contains__('func_call'):
|
||||||
# Get bound plugins for filtering tools
|
# Get bound plugins and MCP servers for filtering tools
|
||||||
bound_plugins = query.variables.get('_pipeline_bound_plugins', None)
|
bound_plugins = query.variables.get('_pipeline_bound_plugins', None)
|
||||||
query.use_funcs = await self.ap.tool_mgr.get_all_tools(bound_plugins)
|
bound_mcp_servers = query.variables.get('_pipeline_bound_mcp_servers', None)
|
||||||
|
query.use_funcs = await self.ap.tool_mgr.get_all_tools(bound_plugins, bound_mcp_servers)
|
||||||
|
|
||||||
|
self.ap.logger.debug(f'Bound plugins: {bound_plugins}')
|
||||||
|
self.ap.logger.debug(f'Bound MCP servers: {bound_mcp_servers}')
|
||||||
|
self.ap.logger.debug(f'Use funcs: {query.use_funcs}')
|
||||||
|
|
||||||
variables = {
|
variables = {
|
||||||
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class RuntimeMCPSession:
|
|||||||
|
|
||||||
server_name: str
|
server_name: str
|
||||||
|
|
||||||
|
server_uuid: str
|
||||||
|
|
||||||
server_config: dict
|
server_config: dict
|
||||||
|
|
||||||
session: ClientSession
|
session: ClientSession
|
||||||
@@ -43,7 +45,6 @@ class RuntimeMCPSession:
|
|||||||
# connected: bool
|
# connected: bool
|
||||||
status: MCPSessionStatus
|
status: MCPSessionStatus
|
||||||
|
|
||||||
|
|
||||||
_lifecycle_task: asyncio.Task | None
|
_lifecycle_task: asyncio.Task | None
|
||||||
|
|
||||||
_shutdown_event: asyncio.Event
|
_shutdown_event: asyncio.Event
|
||||||
@@ -52,6 +53,7 @@ class RuntimeMCPSession:
|
|||||||
|
|
||||||
def __init__(self, server_name: str, server_config: dict, enable: bool, ap: app.Application):
|
def __init__(self, server_name: str, server_config: dict, enable: bool, ap: app.Application):
|
||||||
self.server_name = server_name
|
self.server_name = server_name
|
||||||
|
self.server_uuid = server_config.get('uuid', '')
|
||||||
self.server_config = server_config
|
self.server_config = server_config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
self.enable = enable
|
self.enable = enable
|
||||||
@@ -286,12 +288,14 @@ class MCPLoader(loader.ToolLoader):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = server_config['name']
|
name = server_config['name']
|
||||||
|
uuid = server_config['uuid']
|
||||||
mode = server_config['mode']
|
mode = server_config['mode']
|
||||||
enable = server_config['enable']
|
enable = server_config['enable']
|
||||||
extra_args = server_config.get('extra_args', {})
|
extra_args = server_config.get('extra_args', {})
|
||||||
|
|
||||||
mixed_config = {
|
mixed_config = {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'uuid': uuid,
|
||||||
'mode': mode,
|
'mode': mode,
|
||||||
'enable': enable,
|
'enable': enable,
|
||||||
**extra_args,
|
**extra_args,
|
||||||
@@ -301,11 +305,17 @@ class MCPLoader(loader.ToolLoader):
|
|||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
||||||
async def get_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]:
|
async def get_tools(self, bound_mcp_servers: list[str] | None = None) -> list[resource_tool.LLMTool]:
|
||||||
all_functions = []
|
all_functions = []
|
||||||
|
|
||||||
for session in self.sessions.values():
|
for session in self.sessions.values():
|
||||||
all_functions.extend(session.get_tools())
|
# If bound_mcp_servers is specified, only include tools from those servers
|
||||||
|
if bound_mcp_servers is not None:
|
||||||
|
if session.server_uuid in bound_mcp_servers:
|
||||||
|
all_functions.extend(session.get_tools())
|
||||||
|
else:
|
||||||
|
# If no bound servers specified, include all tools
|
||||||
|
all_functions.extend(session.get_tools())
|
||||||
|
|
||||||
self._last_listed_functions = all_functions
|
self._last_listed_functions = all_functions
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ class ToolManager:
|
|||||||
self.mcp_tool_loader = mcp_loader.MCPLoader(self.ap)
|
self.mcp_tool_loader = mcp_loader.MCPLoader(self.ap)
|
||||||
await self.mcp_tool_loader.initialize()
|
await self.mcp_tool_loader.initialize()
|
||||||
|
|
||||||
async def get_all_tools(self, bound_plugins: list[str] | None = None) -> list[resource_tool.LLMTool]:
|
async def get_all_tools(self, bound_plugins: list[str] | None = None, bound_mcp_servers: list[str] | None = None) -> list[resource_tool.LLMTool]:
|
||||||
"""获取所有函数"""
|
"""获取所有函数"""
|
||||||
all_functions: list[resource_tool.LLMTool] = []
|
all_functions: list[resource_tool.LLMTool] = []
|
||||||
|
|
||||||
all_functions.extend(await self.plugin_tool_loader.get_tools(bound_plugins))
|
all_functions.extend(await self.plugin_tool_loader.get_tools(bound_plugins))
|
||||||
all_functions.extend(await self.mcp_tool_loader.get_tools(bound_plugins))
|
all_functions.extend(await self.mcp_tool_loader.get_tools(bound_mcp_servers))
|
||||||
|
|
||||||
return all_functions
|
return all_functions
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Plus, X } from 'lucide-react';
|
import { Plus, X, Server, Wrench } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Plugin } from '@/app/infra/entities/plugin';
|
import { Plugin } from '@/app/infra/entities/plugin';
|
||||||
|
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';
|
||||||
|
|
||||||
export default function PipelineExtension({
|
export default function PipelineExtension({
|
||||||
@@ -28,8 +29,14 @@ export default function PipelineExtension({
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedPlugins, setSelectedPlugins] = useState<Plugin[]>([]);
|
const [selectedPlugins, setSelectedPlugins] = useState<Plugin[]>([]);
|
||||||
const [allPlugins, setAllPlugins] = useState<Plugin[]>([]);
|
const [allPlugins, setAllPlugins] = useState<Plugin[]>([]);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [selectedMCPServers, setSelectedMCPServers] = useState<MCPServer[]>([]);
|
||||||
const [tempSelectedIds, setTempSelectedIds] = useState<string[]>([]);
|
const [allMCPServers, setAllMCPServers] = useState<MCPServer[]>([]);
|
||||||
|
const [pluginDialogOpen, setPluginDialogOpen] = useState(false);
|
||||||
|
const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
|
||||||
|
const [tempSelectedPluginIds, setTempSelectedPluginIds] = useState<string[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [tempSelectedMCPIds, setTempSelectedMCPIds] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadExtensions();
|
loadExtensions();
|
||||||
@@ -56,6 +63,15 @@ export default function PipelineExtension({
|
|||||||
|
|
||||||
setSelectedPlugins(selected);
|
setSelectedPlugins(selected);
|
||||||
setAllPlugins(data.available_plugins);
|
setAllPlugins(data.available_plugins);
|
||||||
|
|
||||||
|
// Load MCP servers
|
||||||
|
const boundMCPServerIds = new Set(data.bound_mcp_servers || []);
|
||||||
|
const selectedMCP = data.available_mcp_servers.filter((server) =>
|
||||||
|
boundMCPServerIds.has(server.uuid || ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
setSelectedMCPServers(selectedMCP);
|
||||||
|
setAllMCPServers(data.available_mcp_servers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load extensions:', error);
|
console.error('Failed to load extensions:', error);
|
||||||
toast.error(t('pipelines.extensions.loadError'));
|
toast.error(t('pipelines.extensions.loadError'));
|
||||||
@@ -64,7 +80,7 @@ export default function PipelineExtension({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveToBackend = async (plugins: Plugin[]) => {
|
const saveToBackend = async (plugins: Plugin[], mcpServers: MCPServer[]) => {
|
||||||
try {
|
try {
|
||||||
const boundPluginsArray = plugins.map((plugin) => {
|
const boundPluginsArray = plugins.map((plugin) => {
|
||||||
const metadata = plugin.manifest.manifest.metadata;
|
const metadata = plugin.manifest.manifest.metadata;
|
||||||
@@ -74,9 +90,12 @@ export default function PipelineExtension({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const boundMCPServerIds = mcpServers.map((server) => server.uuid || '');
|
||||||
|
|
||||||
await backendClient.updatePipelineExtensions(
|
await backendClient.updatePipelineExtensions(
|
||||||
pipelineId,
|
pipelineId,
|
||||||
boundPluginsArray,
|
boundPluginsArray,
|
||||||
|
boundMCPServerIds,
|
||||||
);
|
);
|
||||||
toast.success(t('pipelines.extensions.saveSuccess'));
|
toast.success(t('pipelines.extensions.saveSuccess'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -92,29 +111,57 @@ export default function PipelineExtension({
|
|||||||
(p) => getPluginId(p) !== pluginId,
|
(p) => getPluginId(p) !== pluginId,
|
||||||
);
|
);
|
||||||
setSelectedPlugins(newPlugins);
|
setSelectedPlugins(newPlugins);
|
||||||
await saveToBackend(newPlugins);
|
await saveToBackend(newPlugins, selectedMCPServers);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenDialog = () => {
|
const handleRemoveMCPServer = async (serverUuid: string) => {
|
||||||
setTempSelectedIds(selectedPlugins.map((p) => getPluginId(p)));
|
const newServers = selectedMCPServers.filter((s) => s.uuid !== serverUuid);
|
||||||
setDialogOpen(true);
|
setSelectedMCPServers(newServers);
|
||||||
|
await saveToBackend(selectedPlugins, newServers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenPluginDialog = () => {
|
||||||
|
setTempSelectedPluginIds(selectedPlugins.map((p) => getPluginId(p)));
|
||||||
|
setPluginDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenMCPDialog = () => {
|
||||||
|
setTempSelectedMCPIds(selectedMCPServers.map((s) => s.uuid || ''));
|
||||||
|
setMcpDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTogglePlugin = (pluginId: string) => {
|
const handleTogglePlugin = (pluginId: string) => {
|
||||||
setTempSelectedIds((prev) =>
|
setTempSelectedPluginIds((prev) =>
|
||||||
prev.includes(pluginId)
|
prev.includes(pluginId)
|
||||||
? prev.filter((id) => id !== pluginId)
|
? prev.filter((id) => id !== pluginId)
|
||||||
: [...prev, pluginId],
|
: [...prev, pluginId],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmSelection = async () => {
|
const handleToggleMCPServer = (serverUuid: string) => {
|
||||||
|
setTempSelectedMCPIds((prev) =>
|
||||||
|
prev.includes(serverUuid)
|
||||||
|
? prev.filter((id) => id !== serverUuid)
|
||||||
|
: [...prev, serverUuid],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmPluginSelection = async () => {
|
||||||
const newSelected = allPlugins.filter((p) =>
|
const newSelected = allPlugins.filter((p) =>
|
||||||
tempSelectedIds.includes(getPluginId(p)),
|
tempSelectedPluginIds.includes(getPluginId(p)),
|
||||||
);
|
);
|
||||||
setSelectedPlugins(newSelected);
|
setSelectedPlugins(newSelected);
|
||||||
setDialogOpen(false);
|
setPluginDialogOpen(false);
|
||||||
await saveToBackend(newSelected);
|
await saveToBackend(newSelected, selectedMCPServers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmMCPSelection = async () => {
|
||||||
|
const newSelected = allMCPServers.filter((s) =>
|
||||||
|
tempSelectedMCPIds.includes(s.uuid || ''),
|
||||||
|
);
|
||||||
|
setSelectedMCPServers(newSelected);
|
||||||
|
setMcpDialogOpen(false);
|
||||||
|
await saveToBackend(selectedPlugins, newSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -128,49 +175,127 @@ export default function PipelineExtension({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
{/* Plugins Section */}
|
||||||
{selectedPlugins.length === 0 ? (
|
<div className="space-y-3">
|
||||||
<div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border">
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
<p className="text-sm text-muted-foreground">
|
{t('pipelines.extensions.pluginsTitle')}
|
||||||
{t('pipelines.extensions.noPluginsSelected')}
|
</h3>
|
||||||
</p>
|
<div className="space-y-2">
|
||||||
</div>
|
{selectedPlugins.length === 0 ? (
|
||||||
) : (
|
<div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border">
|
||||||
<div className="space-y-2">
|
<p className="text-sm text-muted-foreground">
|
||||||
{selectedPlugins.map((plugin) => {
|
{t('pipelines.extensions.noPluginsSelected')}
|
||||||
const pluginId = getPluginId(plugin);
|
</p>
|
||||||
const metadata = plugin.manifest.manifest.metadata;
|
</div>
|
||||||
return (
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedPlugins.map((plugin) => {
|
||||||
|
const pluginId = getPluginId(plugin);
|
||||||
|
const metadata = plugin.manifest.manifest.metadata;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={pluginId}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-3 hover:bg-accent"
|
||||||
|
>
|
||||||
|
<div className="flex-1 flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={backendClient.getPluginIconURL(
|
||||||
|
metadata.author || '',
|
||||||
|
metadata.name,
|
||||||
|
)}
|
||||||
|
alt={metadata.name}
|
||||||
|
className="w-10 h-10 rounded-lg border bg-muted object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">{metadata.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{metadata.author} • v{metadata.version}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 mt-1">
|
||||||
|
<PluginComponentList
|
||||||
|
components={plugin.components}
|
||||||
|
showComponentName={true}
|
||||||
|
showTitle={false}
|
||||||
|
useBadge={true}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!plugin.enabled && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{t('pipelines.extensions.disabled')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleRemovePlugin(pluginId)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleOpenPluginDialog}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
{t('pipelines.extensions.addPlugin')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* MCP Servers Section */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
|
{t('pipelines.extensions.mcpServersTitle')}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedMCPServers.length === 0 ? (
|
||||||
|
<div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t('pipelines.extensions.noMCPServersSelected')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedMCPServers.map((server) => (
|
||||||
<div
|
<div
|
||||||
key={pluginId}
|
key={server.uuid}
|
||||||
className="flex items-center justify-between rounded-lg border p-3 hover:bg-accent"
|
className="flex items-center justify-between rounded-lg border p-3 hover:bg-accent"
|
||||||
>
|
>
|
||||||
<div className="flex-1 flex items-center gap-3">
|
<div className="flex-1 flex items-center gap-3">
|
||||||
<img
|
<div className="w-10 h-10 rounded-lg border bg-muted flex items-center justify-center flex-shrink-0">
|
||||||
src={backendClient.getPluginIconURL(
|
<Server className="h-5 w-5 text-muted-foreground" />
|
||||||
metadata.author || '',
|
|
||||||
metadata.name,
|
|
||||||
)}
|
|
||||||
alt={metadata.name}
|
|
||||||
className="w-10 h-10 rounded-lg border bg-muted object-cover flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-medium">{metadata.name}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{metadata.author} • v{metadata.version}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1 mt-1">
|
|
||||||
<PluginComponentList
|
|
||||||
components={plugin.components}
|
|
||||||
showComponentName={true}
|
|
||||||
showTitle={false}
|
|
||||||
useBadge={true}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{!plugin.enabled && (
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">{server.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{server.mode}
|
||||||
|
</div>
|
||||||
|
{server.runtime_info &&
|
||||||
|
server.runtime_info.status === 'connected' && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-1 mt-1"
|
||||||
|
>
|
||||||
|
<Wrench className="h-3 w-3 text-white" />
|
||||||
|
<span className="text-xs text-white">
|
||||||
|
{t('pipelines.extensions.toolCount', {
|
||||||
|
count: server.runtime_info.tool_count || 0,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!server.enable && (
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
{t('pipelines.extensions.disabled')}
|
{t('pipelines.extensions.disabled')}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -179,23 +304,28 @@ export default function PipelineExtension({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleRemovePlugin(pluginId)}
|
onClick={() => handleRemoveMCPServer(server.uuid || '')}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleOpenMCPDialog}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
{t('pipelines.extensions.addMCPServer')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleOpenDialog} variant="outline" className="w-full">
|
{/* Plugin Selection Dialog */}
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Dialog open={pluginDialogOpen} onOpenChange={setPluginDialogOpen}>
|
||||||
{t('pipelines.extensions.addPlugin')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
||||||
@@ -204,7 +334,7 @@ export default function PipelineExtension({
|
|||||||
{allPlugins.map((plugin) => {
|
{allPlugins.map((plugin) => {
|
||||||
const pluginId = getPluginId(plugin);
|
const pluginId = getPluginId(plugin);
|
||||||
const metadata = plugin.manifest.manifest.metadata;
|
const metadata = plugin.manifest.manifest.metadata;
|
||||||
const isSelected = tempSelectedIds.includes(pluginId);
|
const isSelected = tempSelectedPluginIds.includes(pluginId);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={pluginId}
|
key={pluginId}
|
||||||
@@ -245,10 +375,71 @@ export default function PipelineExtension({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setPluginDialogOpen(false)}
|
||||||
|
>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirmSelection}>
|
<Button onClick={handleConfirmPluginSelection}>
|
||||||
|
{t('common.confirm')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* MCP Server Selection Dialog */}
|
||||||
|
<Dialog open={mcpDialogOpen} onOpenChange={setMcpDialogOpen}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t('pipelines.extensions.selectMCPServers')}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
||||||
|
{allMCPServers.map((server) => {
|
||||||
|
const isSelected = tempSelectedMCPIds.includes(server.uuid || '');
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={server.uuid}
|
||||||
|
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-accent cursor-pointer"
|
||||||
|
onClick={() => handleToggleMCPServer(server.uuid || '')}
|
||||||
|
>
|
||||||
|
<Checkbox checked={isSelected} />
|
||||||
|
<div className="w-10 h-10 rounded-lg border bg-muted flex items-center justify-center flex-shrink-0">
|
||||||
|
<Server className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">{server.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{server.mode}
|
||||||
|
</div>
|
||||||
|
{server.runtime_info &&
|
||||||
|
server.runtime_info.status === 'connected' && (
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<Wrench className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t('pipelines.extensions.toolCount', {
|
||||||
|
count: server.runtime_info.tool_count || 0,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!server.enable && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{t('pipelines.extensions.disabled')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setMcpDialogOpen(false)}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirmMCPSelection}>
|
||||||
{t('common.confirm')}
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
public getPipelineExtensions(uuid: string): Promise<{
|
public getPipelineExtensions(uuid: string): Promise<{
|
||||||
bound_plugins: Array<{ author: string; name: string }>;
|
bound_plugins: Array<{ author: string; name: string }>;
|
||||||
available_plugins: Plugin[];
|
available_plugins: Plugin[];
|
||||||
|
bound_mcp_servers: string[];
|
||||||
|
available_mcp_servers: MCPServer[];
|
||||||
}> {
|
}> {
|
||||||
return this.get(`/api/v1/pipelines/${uuid}/extensions`);
|
return this.get(`/api/v1/pipelines/${uuid}/extensions`);
|
||||||
}
|
}
|
||||||
@@ -180,9 +182,11 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
public updatePipelineExtensions(
|
public updatePipelineExtensions(
|
||||||
uuid: string,
|
uuid: string,
|
||||||
bound_plugins: Array<{ author: string; name: string }>,
|
bound_plugins: Array<{ author: string; name: string }>,
|
||||||
|
bound_mcp_servers: string[],
|
||||||
): 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ const enUS = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
title: 'Extensions',
|
title: 'Extensions',
|
||||||
description:
|
description:
|
||||||
'Install and configure plugins to extend LangBot functionality',
|
'Install and configure plugins to extend functionality, please select them in the pipeline configuration',
|
||||||
createPlugin: 'Create Plugin',
|
createPlugin: 'Create Plugin',
|
||||||
editPlugin: 'Edit Plugin',
|
editPlugin: 'Edit Plugin',
|
||||||
installed: 'Installed',
|
installed: 'Installed',
|
||||||
@@ -438,6 +438,12 @@ const enUS = {
|
|||||||
noPluginsSelected: 'No plugins selected',
|
noPluginsSelected: 'No plugins selected',
|
||||||
addPlugin: 'Add Plugin',
|
addPlugin: 'Add Plugin',
|
||||||
selectPlugins: 'Select Plugins',
|
selectPlugins: 'Select Plugins',
|
||||||
|
pluginsTitle: 'Plugins',
|
||||||
|
mcpServersTitle: 'MCP Servers',
|
||||||
|
noMCPServersSelected: 'No MCP servers selected',
|
||||||
|
addMCPServer: 'Add MCP Server',
|
||||||
|
selectMCPServers: 'Select MCP Servers',
|
||||||
|
toolCount: '{{count}} tools',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'Pipeline Chat',
|
title: 'Pipeline Chat',
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ const jaJP = {
|
|||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '拡張機能',
|
title: '拡張機能',
|
||||||
description: 'LangBotの機能を拡張するプラグインをインストール・設定',
|
description:
|
||||||
|
'LangBotの機能を拡張するプラグインをインストール・設定。流水線設定で使用します',
|
||||||
createPlugin: 'プラグインを作成',
|
createPlugin: 'プラグインを作成',
|
||||||
editPlugin: 'プラグインを編集',
|
editPlugin: 'プラグインを編集',
|
||||||
installed: 'インストール済み',
|
installed: 'インストール済み',
|
||||||
@@ -440,6 +441,12 @@ const jaJP = {
|
|||||||
noPluginsSelected: 'プラグインが選択されていません',
|
noPluginsSelected: 'プラグインが選択されていません',
|
||||||
addPlugin: 'プラグインを追加',
|
addPlugin: 'プラグインを追加',
|
||||||
selectPlugins: 'プラグインを選択',
|
selectPlugins: 'プラグインを選択',
|
||||||
|
pluginsTitle: 'プラグイン',
|
||||||
|
mcpServersTitle: 'MCPサーバー',
|
||||||
|
noMCPServersSelected: 'MCPサーバーが選択されていません',
|
||||||
|
addMCPServer: 'MCPサーバーを追加',
|
||||||
|
selectMCPServers: 'MCPサーバーを選択',
|
||||||
|
toolCount: '{{count}}個のツール',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'パイプラインのチャット',
|
title: 'パイプラインのチャット',
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const zhHans = {
|
|||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '插件扩展',
|
title: '插件扩展',
|
||||||
description: '安装和配置用于扩展 LangBot 功能的插件',
|
description: '安装和配置用于扩展功能的插件,请在流水线配置中选用',
|
||||||
createPlugin: '创建插件',
|
createPlugin: '创建插件',
|
||||||
editPlugin: '编辑插件',
|
editPlugin: '编辑插件',
|
||||||
installed: '已安装',
|
installed: '已安装',
|
||||||
@@ -413,7 +413,7 @@ const zhHans = {
|
|||||||
deleteSuccess: '删除成功',
|
deleteSuccess: '删除成功',
|
||||||
deleteError: '删除失败:',
|
deleteError: '删除失败:',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: '插件集成',
|
title: '扩展集成',
|
||||||
loadError: '加载插件列表失败',
|
loadError: '加载插件列表失败',
|
||||||
saveSuccess: '保存成功',
|
saveSuccess: '保存成功',
|
||||||
saveError: '保存失败',
|
saveError: '保存失败',
|
||||||
@@ -422,6 +422,12 @@ const zhHans = {
|
|||||||
noPluginsSelected: '未选择任何插件',
|
noPluginsSelected: '未选择任何插件',
|
||||||
addPlugin: '添加插件',
|
addPlugin: '添加插件',
|
||||||
selectPlugins: '选择插件',
|
selectPlugins: '选择插件',
|
||||||
|
pluginsTitle: '插件',
|
||||||
|
mcpServersTitle: 'MCP 服务器',
|
||||||
|
noMCPServersSelected: '未选择任何 MCP 服务器',
|
||||||
|
addMCPServer: '添加 MCP 服务器',
|
||||||
|
selectMCPServers: '选择 MCP 服务器',
|
||||||
|
toolCount: '{{count}} 个工具',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流水线对话',
|
title: '流水线对话',
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const zhHant = {
|
|||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '外掛擴展',
|
title: '外掛擴展',
|
||||||
description: '安裝和設定用於擴展 LangBot 功能的外掛',
|
description: '安裝和設定用於擴展功能的外掛,請在流程線配置中選用',
|
||||||
createPlugin: '建立外掛',
|
createPlugin: '建立外掛',
|
||||||
editPlugin: '編輯外掛',
|
editPlugin: '編輯外掛',
|
||||||
installed: '已安裝',
|
installed: '已安裝',
|
||||||
@@ -411,7 +411,7 @@ const zhHant = {
|
|||||||
deleteSuccess: '刪除成功',
|
deleteSuccess: '刪除成功',
|
||||||
deleteError: '刪除失敗:',
|
deleteError: '刪除失敗:',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: '插件集成',
|
title: '擴展集成',
|
||||||
loadError: '載入插件清單失敗',
|
loadError: '載入插件清單失敗',
|
||||||
saveSuccess: '儲存成功',
|
saveSuccess: '儲存成功',
|
||||||
saveError: '儲存失敗',
|
saveError: '儲存失敗',
|
||||||
@@ -420,6 +420,12 @@ const zhHant = {
|
|||||||
noPluginsSelected: '未選擇任何插件',
|
noPluginsSelected: '未選擇任何插件',
|
||||||
addPlugin: '新增插件',
|
addPlugin: '新增插件',
|
||||||
selectPlugins: '選擇插件',
|
selectPlugins: '選擇插件',
|
||||||
|
pluginsTitle: '插件',
|
||||||
|
mcpServersTitle: 'MCP 伺服器',
|
||||||
|
noMCPServersSelected: '未選擇任何 MCP 伺服器',
|
||||||
|
addMCPServer: '新增 MCP 伺服器',
|
||||||
|
selectMCPServers: '選擇 MCP 伺服器',
|
||||||
|
toolCount: '{{count}} 個工具',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流程線對話',
|
title: '流程線對話',
|
||||||
|
|||||||
Reference in New Issue
Block a user