feat: 添加MinIO文件系统支持并优化外部连接器功能
- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等) - 更新外部连接器配置结构,包括BASE_URL和认证令牌设置 - 改进connector provider支持更多类型(official, feishu_bot等) - 实现Mistral模型推理模式支持reasoning_effort参数 - 增强外部连接器策略配置和运行时配置管理 - 添加connector bridge事件验证和安全保护机制 - 优化任务路由逻辑,区分simple_chat和new_task场景 - 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
@ -247,6 +247,9 @@ export default function MCPPage() {
|
||||
};
|
||||
|
||||
const handleDelete = async (serverId: string) => {
|
||||
if (!window.confirm(t('确定删除这个 MCP 服务吗?此操作不可撤销。', 'Delete this MCP server? This action cannot be undone.'))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteMcpServer(serverId);
|
||||
setSelectedServerId((current) => (current === serverId ? null : current));
|
||||
@ -312,9 +315,9 @@ export default function MCPPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<div className="mx-auto max-w-6xl space-y-6 p-4 sm:p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<ServerCog className="w-6 h-6" />
|
||||
{t('工具', 'Tools')}
|
||||
@ -323,8 +326,8 @@ export default function MCPPage() {
|
||||
{t('本地工具和在线工具都通过 MCP Server 暴露;本地工具按类别由真实 stdio MCP 子进程承载。', 'Local and online tools are both exposed through MCP servers. Local tool categories run as real stdio MCP subprocesses.')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => void load(true)}>
|
||||
<div className="flex w-full flex-wrap items-center gap-2 sm:w-auto sm:justify-end">
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => void load(true)}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
{t('刷新', 'Refresh')}
|
||||
</Button>
|
||||
@ -333,12 +336,12 @@ export default function MCPPage() {
|
||||
if (!open) resetForm();
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Button size="sm" className="h-11">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t('新增工具服务', 'Add tool server')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId ? t('编辑 MCP 服务', 'Edit MCP server') : t('新增 MCP 服务', 'Add MCP server')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -346,11 +349,11 @@ export default function MCPPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="id">ID</Label>
|
||||
<Input id="id" value={form.id} onChange={(e) => setForm((s) => ({ ...s, id: e.target.value }))} required disabled={!!editingId} />
|
||||
<Input id="id" className="h-11" value={form.id} onChange={(e) => setForm((s) => ({ ...s, id: e.target.value }))} required disabled={!!editingId} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tool_timeout">{t('工具超时', 'Tool timeout')}</Label>
|
||||
<Input id="tool_timeout" type="number" min="1" value={form.tool_timeout} onChange={(e) => setForm((s) => ({ ...s, tool_timeout: e.target.value }))} />
|
||||
<Input id="tool_timeout" className="h-11" type="number" min="1" value={form.tool_timeout} onChange={(e) => setForm((s) => ({ ...s, tool_timeout: e.target.value }))} />
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
@ -390,6 +393,7 @@ export default function MCPPage() {
|
||||
<Label htmlFor="url">{t('MCP Server 地址', 'MCP server URL')}</Label>
|
||||
<Input
|
||||
id="url"
|
||||
className="h-11"
|
||||
value={form.url}
|
||||
onChange={(e) => setForm((s) => ({ ...s, url: e.target.value }))}
|
||||
placeholder="http://localhost:3001/mcp"
|
||||
@ -403,7 +407,7 @@ export default function MCPPage() {
|
||||
id="auth_mode"
|
||||
value={form.auth_mode}
|
||||
onChange={(e) => setForm((s) => ({ ...s, auth_mode: e.target.value }))}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="none">none</option>
|
||||
<option value="oauth_backend_token">oauth_backend_token</option>
|
||||
@ -452,6 +456,7 @@ export default function MCPPage() {
|
||||
<Label htmlFor="command">{t('命令', 'Command')}</Label>
|
||||
<Input
|
||||
id="command"
|
||||
className="h-11"
|
||||
value={form.command}
|
||||
onChange={(e) => setForm((s) => ({ ...s, command: e.target.value }))}
|
||||
placeholder="npx"
|
||||
@ -462,6 +467,7 @@ export default function MCPPage() {
|
||||
<Label htmlFor="args">{t('参数', 'Arguments')}</Label>
|
||||
<Input
|
||||
id="args"
|
||||
className="h-11"
|
||||
value={form.args}
|
||||
onChange={(e) => setForm((s) => ({ ...s, args: e.target.value }))}
|
||||
placeholder="-y @modelcontextprotocol/server-github"
|
||||
@ -470,11 +476,11 @@ export default function MCPPage() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
<div className="sticky bottom-0 -mx-6 -mb-6 flex justify-end gap-2 border-t bg-background px-6 py-4">
|
||||
<Button type="button" variant="outline" className="h-11" onClick={() => setDialogOpen(false)}>
|
||||
{t('取消', 'Cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
<Button type="submit" className="h-11" disabled={submitting}>
|
||||
{submitting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Plus className="w-4 h-4 mr-2" />}
|
||||
{t('保存', 'Save')}
|
||||
</Button>
|
||||
@ -488,7 +494,7 @@ export default function MCPPage() {
|
||||
{error && (
|
||||
<Card className="border-destructive">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center gap-2 text-destructive text-sm">
|
||||
<div className="flex items-center gap-2 break-words text-sm text-destructive">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
{error}
|
||||
</div>
|
||||
@ -500,9 +506,9 @@ export default function MCPPage() {
|
||||
setToolTab(value as 'local' | 'online');
|
||||
setSelectedServerId(null);
|
||||
}} className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="local">{t('本地工具', 'Local tools')}</TabsTrigger>
|
||||
<TabsTrigger value="online">{t('在线工具', 'Online tools')}</TabsTrigger>
|
||||
<TabsList className="h-auto min-h-11">
|
||||
<TabsTrigger value="local" className="h-11 px-4">{t('本地工具', 'Local tools')}</TabsTrigger>
|
||||
<TabsTrigger value="online" className="h-11 px-4">{t('在线工具', 'Online tools')}</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@ -511,78 +517,73 @@ export default function MCPPage() {
|
||||
{visibleServers.map((server) => (
|
||||
<Card
|
||||
key={server.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedServerId(server.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
setSelectedServerId(server.id);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'cursor-pointer transition-colors',
|
||||
'min-w-0 transition-colors',
|
||||
selectedServerId === server.id && 'border-primary bg-primary/5 shadow-sm'
|
||||
)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-base">{server.name}</CardTitle>
|
||||
<p className="text-xs text-muted-foreground mt-1 font-mono">{server.id}</p>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedServerId(server.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
setSelectedServerId(server.id);
|
||||
}
|
||||
}}
|
||||
className="min-h-11 cursor-pointer rounded-t-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="break-words text-base">{server.name}</CardTitle>
|
||||
<p className="mt-1 break-all font-mono text-xs text-muted-foreground">{server.id}</p>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2 sm:justify-end">
|
||||
<Badge variant="outline">{transportLabel(server.transport, locale)}</Badge>
|
||||
<Badge variant="secondary">{server.category || (server.kind === 'local' ? 'local' : 'online')}</Badge>
|
||||
{server.managed && <Badge variant="outline">{t('内置', 'Built-in')}</Badge>}
|
||||
<Badge variant={server.status === 'connected' ? 'default' : server.status === 'error' ? 'destructive' : 'secondary'}>
|
||||
{serverStatusLabel(server.status, locale)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||
<Badge variant="outline">{transportLabel(server.transport, locale)}</Badge>
|
||||
<Badge variant="secondary">{server.category || (server.kind === 'local' ? 'local' : 'online')}</Badge>
|
||||
{server.managed && <Badge variant="outline">{t('内置', 'Built-in')}</Badge>}
|
||||
<Badge variant={server.status === 'connected' ? 'default' : server.status === 'error' ? 'destructive' : 'secondary'}>
|
||||
{serverStatusLabel(server.status, locale)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3 text-sm">
|
||||
{server.url && <div><span className="font-medium">URL:</span> <span className="text-muted-foreground break-all">{server.url}</span></div>}
|
||||
{server.command && <div><span className="font-medium">{t('命令:', 'Command:')}</span> <span className="text-muted-foreground">{server.command} {(server.args || []).join(' ')}</span></div>}
|
||||
{server.auth_mode && server.auth_mode !== 'none' && <div><span className="font-medium">{t('鉴权:', 'Auth:')}</span> <span className="text-muted-foreground">{server.auth_mode}</span></div>}
|
||||
{(server.auth_audience || server.auth_mode === 'oauth_backend_token') && (
|
||||
<div><span className="font-medium">Audience:</span> <span className="text-muted-foreground">{server.auth_audience || resolveAuthAudience(server.id)}</span></div>
|
||||
)}
|
||||
{(server.auth_scopes || []).length > 0 && <div><span className="font-medium">Scopes:</span> <span className="text-muted-foreground break-all">{(server.auth_scopes || []).join(', ')}</span></div>}
|
||||
{server.auth_mode === 'oauth_backend_token' && (!server.auth_scopes || server.auth_scopes.length === 0) && (
|
||||
<div><span className="font-medium">Scopes:</span> <span className="text-muted-foreground">{t('由 AuthZ 动态决定', 'Derived from AuthZ')}</span></div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 flex-wrap text-xs text-muted-foreground">
|
||||
<span>{t(`${discoveredToolCount(server.id, tools, server.tool_count)} 个工具`, `${discoveredToolCount(server.id, tools, server.tool_count)} tools`)}</span>
|
||||
<span>{selectedServerId === server.id ? t('已选中', 'Selected') : t('点击查看工具', 'Click to view tools')}</span>
|
||||
{server.last_error && <span className="text-rose-300">{server.last_error}</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
{!server.managed && (
|
||||
<Button variant="outline" size="sm" onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openEdit(server);
|
||||
}}>
|
||||
{t('编辑', 'Edit')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-0 text-sm">
|
||||
{server.url && <div className="break-words"><span className="font-medium">URL:</span> <span className="break-all text-muted-foreground">{server.url}</span></div>}
|
||||
{server.command && <div className="break-words"><span className="font-medium">{t('命令:', 'Command:')}</span> <span className="break-all text-muted-foreground">{server.command} {(server.args || []).join(' ')}</span></div>}
|
||||
{server.auth_mode && server.auth_mode !== 'none' && <div className="break-words"><span className="font-medium">{t('鉴权:', 'Auth:')}</span> <span className="break-all text-muted-foreground">{server.auth_mode}</span></div>}
|
||||
{(server.auth_audience || server.auth_mode === 'oauth_backend_token') && (
|
||||
<div className="break-words"><span className="font-medium">Audience:</span> <span className="break-all text-muted-foreground">{server.auth_audience || resolveAuthAudience(server.id)}</span></div>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void handleTest(server.id);
|
||||
}} disabled={testingId === server.id}>
|
||||
{testingId === server.id ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <TestTube2 className="w-4 h-4 mr-2" />}
|
||||
{t('测试', 'Test')}
|
||||
{(server.auth_scopes || []).length > 0 && <div className="break-words"><span className="font-medium">Scopes:</span> <span className="break-all text-muted-foreground">{(server.auth_scopes || []).join(', ')}</span></div>}
|
||||
{server.auth_mode === 'oauth_backend_token' && (!server.auth_scopes || server.auth_scopes.length === 0) && (
|
||||
<div className="break-words"><span className="font-medium">Scopes:</span> <span className="text-muted-foreground">{t('由 AuthZ 动态决定', 'Derived from AuthZ')}</span></div>
|
||||
)}
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{t(`${discoveredToolCount(server.id, tools, server.tool_count)} 个工具`, `${discoveredToolCount(server.id, tools, server.tool_count)} tools`)}</span>
|
||||
<span>{selectedServerId === server.id ? t('已选中', 'Selected') : t('点击查看工具', 'Click to view tools')}</span>
|
||||
{server.last_error && <span className="break-all text-destructive">{server.last_error}</span>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
<CardContent className="flex flex-wrap items-center justify-end gap-2 pt-0">
|
||||
{!server.managed && (
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => openEdit(server)}>
|
||||
{t('编辑', 'Edit')}
|
||||
</Button>
|
||||
{!server.managed && (
|
||||
<Button variant="outline" size="sm" onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void handleDelete(server.id);
|
||||
}}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
{t('删除', 'Delete')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => void handleTest(server.id)} disabled={testingId === server.id}>
|
||||
{testingId === server.id ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <TestTube2 className="w-4 h-4 mr-2" />}
|
||||
{t('测试', 'Test')}
|
||||
</Button>
|
||||
{!server.managed && (
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => void handleDelete(server.id)}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
{t('删除', 'Delete')}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
@ -595,9 +596,9 @@ export default function MCPPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Card className="min-w-0">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<CardTitle className="flex items-center gap-2 break-words text-base">
|
||||
<Wrench className="w-4 h-4" />
|
||||
{selectedServer ? t(`${selectedServer.name} 的工具`, `${selectedServer.name} tools`) : t('工具详情', 'Tool details')}
|
||||
</CardTitle>
|
||||
@ -613,12 +614,12 @@ export default function MCPPage() {
|
||||
)}
|
||||
{selectedToolGroup && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">{selectedToolGroup.server_id}</div>
|
||||
<div className="break-all text-sm font-medium">{selectedToolGroup.server_id}</div>
|
||||
<div className="space-y-2">
|
||||
{selectedToolGroup.tools.map((tool) => (
|
||||
<div key={String(tool.name)} className="rounded-md border border-border/70 px-3 py-2 bg-background/60">
|
||||
<div className="text-sm font-medium">{String(tool.tool_name || tool.name)}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1 whitespace-pre-wrap break-words">
|
||||
<div key={String(tool.name)} className="min-w-0 rounded-md border border-border/70 bg-background/60 px-3 py-2">
|
||||
<div className="break-all text-sm font-medium">{String(tool.tool_name || tool.name)}</div>
|
||||
<div className="mt-1 whitespace-pre-wrap break-words text-xs text-muted-foreground">
|
||||
{String(tool.description || '—')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user