feat: 添加swarms团队编排功能并优化agent委派系统

- 引入AgentTeamOrchestrator支持多agent协同任务执行
- 增加第三方swarms库依赖并配置git协议替换以改善包管理
- 扩展DelegationManager支持团队任务调度和进度跟踪
- 实现中文bigram分词算法提升中文任务检索准确性
- 调整A2AClient和DelegationManager超时时间从30秒增至600秒
- 优化AgentRunResult状态判断逻辑增加有意义摘要检测
- 修改Dockerfile配置npm仓库镜像地址和git协议映射
- 更新CLI命令行接口支持网关端口配置传递
- 调整提供者超时配置机制增强请求稳定性
- 移除过时的support_group字段简化agent描述符结构
- 增强错误处理和进度事件报告机制改进用户体验
This commit is contained in:
2026-04-14 14:34:23 +08:00
parent fee9007da6
commit cdfc222c9f
85 changed files with 5443 additions and 1392 deletions

View File

@ -15,6 +15,9 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea';
import type { AppLocale } from '@/lib/i18n/core';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
type McpFormMode = 'remote' | 'install';
@ -81,20 +84,22 @@ function resolveAuthzMcpScopes(authzStatus: AuthzStatus | null, serverId: string
};
}
function serverStatusLabel(status?: string | null) {
if (status === 'connected') return '已连接';
if (status === 'error') return '异常';
if (status === 'disconnected' || !status) return '未连接';
function serverStatusLabel(status: string | null | undefined, locale: AppLocale) {
if (status === 'connected') return pickAppText(locale, '已连接', 'Connected');
if (status === 'error') return pickAppText(locale, '异常', 'Error');
if (status === 'disconnected' || !status) return pickAppText(locale, '未连接', 'Disconnected');
return status;
}
function transportLabel(transport?: string) {
if (transport === 'stdio') return '标准输入输出';
function transportLabel(transport: string | undefined, locale: AppLocale) {
if (transport === 'stdio') return pickAppText(locale, '标准输入输出', 'Standard I/O');
if (transport === 'http') return 'HTTP';
return transport || '-';
}
export default function MCPPage() {
const { locale } = useAppI18n();
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
const cachedServers = useChatStore((s) => s.mcpRegistry);
const cachedTools = useChatStore((s) => s.mcpToolRegistry);
const setCachedServers = useChatStore((s) => s.setMcpRegistry);
@ -134,7 +139,7 @@ export default function MCPPage() {
setAuthzStatus(authzData);
setSelectedServerId((current) => (current && nextServers.some((server) => server.id === current) ? current : null));
} catch (err: any) {
setError(err.message || '加载 MCP 服务失败');
setError(err.message || t('加载 MCP 服务失败', 'Failed to load MCP servers'));
} finally {
if (background) {
setRefreshing(false);
@ -172,7 +177,7 @@ export default function MCPPage() {
if (!value.trim()) return {};
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${label} 必须是 JSON 对象`);
throw new Error(`${label} ${t('必须是 JSON 对象', 'must be a JSON object')}`);
}
return parsed as Record<string, string>;
};
@ -187,16 +192,16 @@ export default function MCPPage() {
const command = form.command.trim();
const toolTimeout = Number(form.tool_timeout || 30);
if (!id) {
throw new Error('ID 不能为空');
throw new Error(t('ID 不能为空', 'ID cannot be empty'));
}
if (!Number.isFinite(toolTimeout) || toolTimeout < 1) {
throw new Error('工具超时必须大于 0');
throw new Error(t('工具超时必须大于 0', 'Tool timeout must be greater than 0'));
}
if (form.mode === 'remote' && !url) {
throw new Error('请输入 MCP Server 地址');
throw new Error(t('请输入 MCP Server 地址', 'Enter an MCP server URL'));
}
if (form.mode === 'install' && !command) {
throw new Error('请输入安装或启动命令');
throw new Error(t('请输入安装或启动命令', 'Enter an install or launch command'));
}
const authMode = form.mode === 'remote' ? (form.auth_mode || 'none') : 'none';
@ -209,7 +214,7 @@ export default function MCPPage() {
: [],
env: {},
url: form.mode === 'remote' ? url : '',
headers: form.mode === 'remote' ? parseObjectField('请求头', form.headers) : {},
headers: form.mode === 'remote' ? parseObjectField(t('请求头', 'Headers'), form.headers) : {},
auth_mode: authMode,
auth_audience: authAudience,
auth_scopes: [],
@ -224,7 +229,7 @@ export default function MCPPage() {
resetForm();
await load();
} catch (err: any) {
setError(err.message || '保存 MCP 服务失败');
setError(err.message || t('保存 MCP 服务失败', 'Failed to save the MCP server'));
} finally {
setSubmitting(false);
}
@ -236,7 +241,7 @@ export default function MCPPage() {
setSelectedServerId((current) => (current === serverId ? null : current));
await load();
} catch (err: any) {
setError(err.message || '删除 MCP 服务失败');
setError(err.message || t('删除 MCP 服务失败', 'Failed to delete the MCP server'));
}
};
@ -246,7 +251,7 @@ export default function MCPPage() {
await testMcpServer(serverId);
await load(true);
} catch (err: any) {
setError(err.message || '测试 MCP 服务失败');
setError(err.message || t('测试 MCP 服务失败', 'Failed to test the MCP server'));
} finally {
setTestingId(null);
}
@ -257,20 +262,32 @@ export default function MCPPage() {
const showAuthzPreview = form.auth_mode === 'oauth_backend_token';
const selectedServer = selectedServerId ? servers.find((server) => server.id === selectedServerId) || null : null;
const selectedToolGroup = selectedServerId ? tools.find((group) => group.server_id === selectedServerId) || null : null;
let authzHint = '无需手动填写。Audience 会按 MCP ID 自动生成Scopes 按 AuthZ 当前权限动态决定。';
let authzHint = t(
'无需手动填写。Audience 会按 MCP ID 自动生成Scopes 按 AuthZ 当前权限动态决定。',
'No manual input is required. The audience is generated from the MCP ID and scopes follow current AuthZ permissions.'
);
if (showAuthzPreview) {
if (!form.id.trim()) {
authzHint = '先填写 MCP IDAudience 会自动生成为 mcp:<id>。';
authzHint = t('先填写 MCP IDAudience 会自动生成为 mcp:<id>。', 'Enter the MCP ID first. The audience will become mcp:<id>.');
} else if (!authzStatus?.enabled) {
authzHint = '当前 workspace 没启用 AuthZ选择 oauth_backend_token 后将无法申请访问 token。';
authzHint = t(
'当前 workspace 没启用 AuthZ选择 oauth_backend_token 后将无法申请访问 token。',
'AuthZ is not enabled for this workspace, so oauth_backend_token cannot request access tokens.'
);
} else if (!authzStatus.local_backend.registered) {
authzHint = '当前 backend 还没有在 AuthZ 注册,暂时无法读取权限或申请 token。';
authzHint = t(
'当前 backend 还没有在 AuthZ 注册,暂时无法读取权限或申请 token。',
'The backend is not registered in AuthZ yet, so permissions and access tokens are unavailable.'
);
} else if (authzStatus.error) {
authzHint = `读取 AuthZ 权限失败:${authzStatus.error}`;
authzHint = t(`读取 AuthZ 权限失败:${authzStatus.error}`, `Failed to read AuthZ permissions: ${authzStatus.error}`);
} else if (!authzMcpScopes.available || !authzMcpScopes.enabled) {
authzHint = `AuthZ 里还没有为 ${authAudience || '这个 MCP'} 开启权限,保存后调用会返回 403。`;
authzHint = t(
`AuthZ 里还没有为 ${authAudience || '这个 MCP'} 开启权限,保存后调用会返回 403。`,
`AuthZ does not have permissions enabled for ${authAudience || 'this MCP'} yet, so calls will return 403 after saving.`
);
} else {
authzHint = `已从 AuthZ 读取到 ${authAudience} 的当前权限。`;
authzHint = t(`已从 AuthZ 读取到 ${authAudience} 的当前权限。`, `Loaded current permissions for ${authAudience} from AuthZ.`);
}
}
@ -288,16 +305,16 @@ export default function MCPPage() {
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<ServerCog className="w-6 h-6" />
MCP
{t('MCP 服务', 'MCP servers')}
</h1>
<p className="text-sm text-muted-foreground mt-1">
MCP
{t('管理 MCP 服务配置、连通性和当前已发现的工具。', 'Manage MCP server configuration, connectivity, and discovered tools.')}
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => void load(true)}>
<RefreshCw className={`w-4 h-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
{t('刷新', 'Refresh')}
</Button>
<Dialog open={dialogOpen} onOpenChange={(open) => {
setDialogOpen(open);
@ -306,12 +323,12 @@ export default function MCPPage() {
<DialogTrigger asChild>
<Button size="sm">
<Plus className="w-4 h-4 mr-2" />
MCP
{t('新增 MCP', 'Add MCP')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{editingId ? '编辑 MCP 服务' : '新增 MCP 服务'}</DialogTitle>
<DialogTitle>{editingId ? t('编辑 MCP 服务', 'Edit MCP server') : t('新增 MCP 服务', 'Add MCP server')}</DialogTitle>
</DialogHeader>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@ -320,7 +337,7 @@ export default function MCPPage() {
<Input id="id" 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"></Label>
<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 }))} />
</div>
</div>
@ -330,24 +347,24 @@ export default function MCPPage() {
className="space-y-4"
>
<div className="space-y-2">
<Label></Label>
<Label>{t('接入方式', 'Connection mode')}</Label>
<TabsList className="grid h-auto w-full grid-cols-1 gap-2 bg-transparent p-0 sm:grid-cols-2">
<TabsTrigger
value="remote"
className="h-full flex-col items-start gap-1 rounded-lg border border-border/70 bg-background/80 px-4 py-3 text-left whitespace-normal data-[state=active]:border-primary"
>
<span className="text-sm font-medium"> MCP Server</span>
<span className="text-sm font-medium">{t('连接已有 MCP Server', 'Connect to an existing MCP server')}</span>
<span className="text-xs font-normal text-muted-foreground">
MCP URL
{t('适合已经部署好的远程 MCP 服务,填写 URL、请求头和鉴权即可。', 'Use this for a remote MCP server that is already deployed. Provide the URL, headers, and auth settings.')}
</span>
</TabsTrigger>
<TabsTrigger
value="install"
className="h-full flex-col items-start gap-1 rounded-lg border border-border/70 bg-background/80 px-4 py-3 text-left whitespace-normal data-[state=active]:border-primary"
>
<span className="text-sm font-medium"></span>
<span className="text-sm font-medium">{t('命令安装并启动', 'Install and launch with a command')}</span>
<span className="text-xs font-normal text-muted-foreground">
`npx``uvx` MCP
{t('适合本机通过 `npx`、`uvx` 或其他命令启动 MCP 进程。', 'Use this when the MCP process runs locally via `npx`, `uvx`, or another command.')}
</span>
</TabsTrigger>
</TabsList>
@ -355,10 +372,10 @@ export default function MCPPage() {
<TabsContent value="remote" className="mt-0 rounded-lg border border-border/70 p-4 space-y-4">
<div className="text-sm text-muted-foreground">
MCP Server访
{t('连接一个已经存在的 MCP Server前端只保存访问地址、请求头和鉴权配置。', 'Connect to an existing MCP server. The frontend only stores the address, headers, and auth settings.')}
</div>
<div className="space-y-2">
<Label htmlFor="url">MCP Server </Label>
<Label htmlFor="url">{t('MCP Server 地址', 'MCP server URL')}</Label>
<Input
id="url"
value={form.url}
@ -369,7 +386,7 @@ export default function MCPPage() {
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="auth_mode"></Label>
<Label htmlFor="auth_mode">{t('鉴权模式', 'Auth mode')}</Label>
<select
id="auth_mode"
value={form.auth_mode}
@ -381,20 +398,20 @@ export default function MCPPage() {
</select>
</div>
<div className="space-y-2 sm:col-span-2">
<Label>AuthZ </Label>
<Label>{t('AuthZ 权限', 'AuthZ permissions')}</Label>
<div className="rounded-md border border-border/70 bg-muted/30 px-3 py-3 text-sm space-y-2">
<div className="flex flex-col gap-1">
<span className="text-muted-foreground">Audience</span>
<span className="font-mono text-xs break-all">
{showAuthzPreview ? (authAudience || '填写 MCP ID 后自动生成') : '关闭鉴权时无需配置'}
{showAuthzPreview ? (authAudience || t('填写 MCP ID 后自动生成', 'Generated after you enter the MCP ID')) : t('关闭鉴权时无需配置', 'Not required when auth is disabled')}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-muted-foreground">Scopes</span>
<span className="text-xs break-words">
{showAuthzPreview
? (authzMcpScopes.scopes.length > 0 ? authzMcpScopes.scopes.join(', ') : '由 AuthZ 当前权限动态决定')
: '关闭鉴权时无需配置'}
? (authzMcpScopes.scopes.length > 0 ? authzMcpScopes.scopes.join(', ') : t('由 AuthZ 当前权限动态决定', 'Derived from current AuthZ permissions'))
: t('关闭鉴权时无需配置', 'Not required when auth is disabled')}
</span>
</div>
<div className="text-xs text-muted-foreground">
@ -404,7 +421,7 @@ export default function MCPPage() {
</div>
</div>
<div className="space-y-2">
<Label htmlFor="headers"> JSON</Label>
<Label htmlFor="headers">{t('请求头 JSON', 'Headers JSON')}</Label>
<Textarea
id="headers"
rows={8}
@ -416,11 +433,11 @@ export default function MCPPage() {
<TabsContent value="install" className="mt-0 rounded-lg border border-border/70 p-4 space-y-4">
<div className="text-sm text-muted-foreground">
MCP `npx``uvx`
{t('通过命令安装并启动本地 MCP 进程,适合 `npx`、`uvx`、脚本或容器方式。', 'Install and launch a local MCP process with a command, such as `npx`, `uvx`, a script, or a container.')}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="command"></Label>
<Label htmlFor="command">{t('命令', 'Command')}</Label>
<Input
id="command"
value={form.command}
@ -430,7 +447,7 @@ export default function MCPPage() {
/>
</div>
<div className="space-y-2">
<Label htmlFor="args"></Label>
<Label htmlFor="args">{t('参数', 'Arguments')}</Label>
<Input
id="args"
value={form.args}
@ -443,11 +460,11 @@ export default function MCPPage() {
</Tabs>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
{t('取消', 'Cancel')}
</Button>
<Button type="submit" disabled={submitting}>
{submitting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Plus className="w-4 h-4 mr-2" />}
{t('保存', 'Save')}
</Button>
</div>
</form>
@ -493,27 +510,27 @@ export default function MCPPage() {
<p className="text-xs text-muted-foreground mt-1 font-mono">{server.id}</p>
</div>
<div className="flex items-center gap-2 flex-wrap justify-end">
<Badge variant="outline">{transportLabel(server.transport)}</Badge>
<Badge variant="outline">{transportLabel(server.transport, locale)}</Badge>
<Badge variant={server.status === 'connected' ? 'default' : server.status === 'error' ? 'destructive' : 'secondary'}>
{serverStatusLabel(server.status)}
{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"></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"></span> <span className="text-muted-foreground">{server.auth_mode}</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"> AuthZ </span></div>
<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>{server.tool_count || 0} </span>
<span>{selectedServerId === server.id ? '已选中' : '点击查看工具'}</span>
<span>{t(`${server.tool_count || 0} 个工具`, `${server.tool_count || 0} 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">
@ -521,21 +538,21 @@ export default function MCPPage() {
event.stopPropagation();
openEdit(server);
}}>
{t('编辑', 'Edit')}
</Button>
<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')}
</Button>
<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>
</CardContent>
@ -544,7 +561,7 @@ export default function MCPPage() {
{servers.length === 0 && (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
MCP
{t('暂无 MCP 服务。', 'There are no MCP servers yet.')}
</CardContent>
</Card>
)}
@ -554,17 +571,17 @@ export default function MCPPage() {
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Wrench className="w-4 h-4" />
{selectedServer ? `${selectedServer.name} 的工具` : 'MCP 工具'}
{selectedServer ? t(`${selectedServer.name} 的工具`, `${selectedServer.name} tools`) : t('MCP 工具', 'MCP tools')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!selectedServer && (
<div className="py-10 text-sm text-muted-foreground text-center">
MCP
{t('点击左侧 MCP 服务后,这里才会显示对应的已发现工具。', 'Select an MCP server on the left to show its discovered tools here.')}
</div>
)}
{selectedServer && !selectedToolGroup && (
<div className="text-sm text-muted-foreground"> MCP </div>
<div className="text-sm text-muted-foreground">{t('这个 MCP 暂时还没有发现任何工具。', 'No tools have been discovered for this MCP yet.')}</div>
)}
{selectedToolGroup && (
<div className="space-y-2">