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:
2026-06-05 11:46:40 +08:00
parent 236ac19789
commit 2c5205b06e
120 changed files with 8321 additions and 1865 deletions

View File

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