Files
beaver_project/app-instance/frontend/app/(app)/agents/page.tsx
steven_li cdfc222c9f feat: 添加swarms团队编排功能并优化agent委派系统
- 引入AgentTeamOrchestrator支持多agent协同任务执行
- 增加第三方swarms库依赖并配置git协议替换以改善包管理
- 扩展DelegationManager支持团队任务调度和进度跟踪
- 实现中文bigram分词算法提升中文任务检索准确性
- 调整A2AClient和DelegationManager超时时间从30秒增至600秒
- 优化AgentRunResult状态判断逻辑增加有意义摘要检测
- 修改Dockerfile配置npm仓库镜像地址和git协议映射
- 更新CLI命令行接口支持网关端口配置传递
- 调整提供者超时配置机制增强请求稳定性
- 移除过时的support_group字段简化agent描述符结构
- 增强错误处理和进度事件报告机制改进用户体验
2026-04-14 14:34:23 +08:00

814 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useCallback, useEffect, useState } from 'react';
import {
AlertCircle,
Bot,
ChevronDown,
Loader2,
Pencil,
Plus,
RefreshCw,
Tags,
Trash2,
} from 'lucide-react';
import {
addAgent,
createSubagent,
deleteAgent,
deleteSubagent,
listAgents,
listSubagents,
refreshAgents,
updateSubagent,
} from '@/lib/api';
import { useChatStore } from '@/lib/store';
import type { UiAgentDescriptor, UiSubagentDescriptor } from '@/types';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
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';
const EMPTY_AGENT_FORM = {
id: '',
name: '',
description: '',
base_url: '',
endpoint: '',
card_url: '',
auth_env: '',
auth_mode: 'none',
auth_audience: '',
auth_scopes: '',
tags: '',
aliases: '',
};
const EMPTY_SUBAGENT_FORM = {
id: '',
name: '',
description: '',
system_prompt: '',
model: '',
delegation_mode: 'remote_a2a_only',
enabled: true,
allow_mcp: true,
tags: '',
aliases: '',
metadata_json: '{}',
mcp_servers_json: '{}',
};
function formatJson(value: Record<string, unknown>): string {
return JSON.stringify(value, null, 2);
}
function parseJsonObject(raw: string, label: string, locale: AppLocale): Record<string, unknown> {
const probe = raw.trim();
if (!probe) {
return {};
}
let parsed: unknown;
try {
parsed = JSON.parse(probe);
} catch {
throw new Error(`${label} ${pickAppText(locale, '需要是合法 JSON', 'must be valid JSON')}`);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${label} ${pickAppText(locale, '需要是 JSON 对象', 'must be a JSON object')}`);
}
return parsed as Record<string, unknown>;
}
function parseNestedJsonObject(raw: string, label: string, locale: AppLocale): Record<string, Record<string, unknown>> {
const parsed = parseJsonObject(raw, label, locale);
for (const [key, value] of Object.entries(parsed)) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(
pickAppText(
locale,
`${label} 中的 ${key} 必须是 JSON 对象`,
`${key} in ${label} must be a JSON object`
)
);
}
}
return parsed as Record<string, Record<string, unknown>>;
}
function agentSourceLabel(source: UiAgentDescriptor['source'], locale: AppLocale): string {
switch (source) {
case 'workspace':
return pickAppText(locale, '工作区', 'Workspace');
case 'plugin':
return pickAppText(locale, '插件', 'Plugin');
case 'skill':
return pickAppText(locale, '技能', 'Skill');
default:
return pickAppText(locale, '内置', 'Built-in');
}
}
export default function AgentsPage() {
const { locale } = useAppI18n();
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
const cachedAgents = useChatStore((s) => s.agentRegistry);
const setCachedAgents = useChatStore((s) => s.setAgentRegistry);
const [agents, setAgents] = useState<UiAgentDescriptor[]>(cachedAgents);
const [subagents, setSubagents] = useState<UiSubagentDescriptor[]>([]);
const [loading, setLoading] = useState(cachedAgents.length === 0);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [agentDialogOpen, setAgentDialogOpen] = useState(false);
const [subagentDialogOpen, setSubagentDialogOpen] = useState(false);
const [agentSubmitting, setAgentSubmitting] = useState(false);
const [subagentSubmitting, setSubagentSubmitting] = useState(false);
const [agentAdvancedOpen, setAgentAdvancedOpen] = useState(false);
const [subagentAdvancedOpen, setSubagentAdvancedOpen] = useState(false);
const [agentForm, setAgentForm] = useState(EMPTY_AGENT_FORM);
const [subagentForm, setSubagentForm] = useState(EMPTY_SUBAGENT_FORM);
const [editingSubagentId, setEditingSubagentId] = useState<string | null>(null);
const load = useCallback(async (background = false) => {
if (background) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);
try {
const [agentData, subagentData] = await Promise.all([
listAgents(),
listSubagents(),
]);
const nextAgents = Array.isArray(agentData) ? agentData : [];
const nextSubagents = Array.isArray(subagentData) ? subagentData : [];
setAgents(nextAgents);
setSubagents(nextSubagents);
setCachedAgents(nextAgents);
} catch (err: any) {
setError(err.message || t('加载智能体失败', 'Failed to load agents'));
} finally {
if (background) {
setRefreshing(false);
} else {
setLoading(false);
}
}
}, [setCachedAgents]);
useEffect(() => {
void load(cachedAgents.length > 0);
}, [cachedAgents.length, load]);
const handleRefresh = async () => {
setError(null);
setRefreshing(true);
try {
const [agentData, subagentData] = await Promise.all([
refreshAgents(),
listSubagents(),
]);
const nextAgents = agentData.agents || [];
const nextSubagents = Array.isArray(subagentData) ? subagentData : [];
setAgents(nextAgents);
setSubagents(nextSubagents);
setCachedAgents(nextAgents);
} catch (err: any) {
setError(err.message || t('刷新智能体失败', 'Failed to refresh agents'));
} finally {
setRefreshing(false);
}
};
const handleAgentDialogOpenChange = (open: boolean) => {
setAgentDialogOpen(open);
if (!open) {
setAgentAdvancedOpen(false);
setAgentForm(EMPTY_AGENT_FORM);
}
};
const handleSubagentDialogOpenChange = (open: boolean) => {
setSubagentDialogOpen(open);
if (!open) {
setSubagentAdvancedOpen(false);
setEditingSubagentId(null);
setSubagentForm(EMPTY_SUBAGENT_FORM);
}
};
const handleCreateAgent = async (e: React.FormEvent) => {
e.preventDefault();
const hasAddress = [agentForm.base_url, agentForm.endpoint, agentForm.card_url].some((value) => value.trim());
if (!hasAddress) {
setError(t('请至少填写 A2A 部署地址、接口地址或卡片地址', 'Enter at least an A2A base URL, endpoint, or card URL'));
return;
}
setAgentSubmitting(true);
setError(null);
try {
await addAgent({
id: agentForm.id || undefined,
name: agentForm.name || undefined,
description: agentForm.description || undefined,
protocol: 'a2a',
base_url: agentForm.base_url || undefined,
endpoint: agentForm.endpoint || undefined,
card_url: agentForm.card_url || undefined,
auth_env: agentForm.auth_env || undefined,
auth_mode: agentForm.auth_mode || 'none',
auth_audience: agentForm.auth_mode === 'none' ? undefined : agentForm.auth_audience || undefined,
auth_scopes: agentForm.auth_mode === 'none'
? []
: agentForm.auth_scopes.split(',').map((item) => item.trim()).filter(Boolean),
tags: agentForm.tags.split(',').map((item) => item.trim()).filter(Boolean),
aliases: agentForm.aliases.split(',').map((item) => item.trim()).filter(Boolean),
});
handleAgentDialogOpenChange(false);
await load(true);
} catch (err: any) {
setError(err.message || t('新增智能体失败', 'Failed to create the agent'));
} finally {
setAgentSubmitting(false);
}
};
const handleDeleteAgent = async (agentId: string) => {
try {
await deleteAgent(agentId);
await load(true);
} catch (err: any) {
setError(err.message || t('删除智能体失败', 'Failed to delete the agent'));
}
};
const handleEditSubagent = (subagent: UiSubagentDescriptor) => {
setEditingSubagentId(subagent.id);
setSubagentForm({
id: subagent.id,
name: subagent.name,
description: subagent.description,
system_prompt: subagent.system_prompt || '',
model: subagent.model || '',
delegation_mode: subagent.delegation_mode || 'remote_a2a_only',
enabled: subagent.enabled,
allow_mcp: subagent.allow_mcp,
tags: (subagent.tags || []).join(', '),
aliases: (subagent.aliases || []).join(', '),
metadata_json: formatJson(subagent.metadata || {}),
mcp_servers_json: formatJson(subagent.mcp_servers || {}),
});
setSubagentDialogOpen(true);
};
const handleSaveSubagent = async (e: React.FormEvent) => {
e.preventDefault();
if (!subagentForm.id.trim()) {
setError(t('Sub-agent ID 不能为空', 'Sub-agent ID cannot be empty'));
return;
}
setSubagentSubmitting(true);
setError(null);
try {
const payload = {
id: subagentForm.id.trim(),
name: subagentForm.name.trim() || subagentForm.id.trim(),
description: subagentForm.description.trim() || subagentForm.name.trim() || subagentForm.id.trim(),
system_prompt: subagentForm.system_prompt,
model: subagentForm.model.trim() || undefined,
enabled: subagentForm.enabled,
delegation_mode: subagentForm.delegation_mode,
allow_mcp: subagentForm.allow_mcp,
tags: subagentForm.tags.split(',').map((item) => item.trim()).filter(Boolean),
aliases: subagentForm.aliases.split(',').map((item) => item.trim()).filter(Boolean),
metadata: parseJsonObject(subagentForm.metadata_json, 'Metadata', locale),
mcp_servers: parseNestedJsonObject(subagentForm.mcp_servers_json, 'MCP Servers', locale),
};
if (editingSubagentId) {
await updateSubagent(editingSubagentId, payload);
} else {
await createSubagent(payload);
}
handleSubagentDialogOpenChange(false);
await load(true);
} catch (err: any) {
setError(err.message || t('保存 Sub-Agent 失败', 'Failed to save the sub-agent'));
} finally {
setSubagentSubmitting(false);
}
};
const handleDeleteManagedSubagent = async (subagentId: string) => {
try {
await deleteSubagent(subagentId);
await load(true);
} catch (err: any) {
setError(err.message || t('删除 Sub-Agent 失败', 'Failed to delete the sub-agent'));
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
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>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Bot className="w-6 h-6" />
{t('智能体', 'Agents')}
</h1>
<p className="text-sm text-muted-foreground mt-1">
{t('管理外部 A2A 智能体,以及持久化的本地 Sub-Agent。', 'Manage external A2A agents and persistent local sub-agents.')}
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Button variant="outline" size="sm" onClick={handleRefresh}>
<RefreshCw className={`w-4 h-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
{t('刷新', 'Refresh')}
</Button>
<Dialog open={agentDialogOpen} onOpenChange={handleAgentDialogOpenChange}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<Plus className="w-4 h-4 mr-2" />
{t('新增智能体', 'Add agent')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{t('新增工作区智能体', 'Add workspace agent')}</DialogTitle>
</DialogHeader>
<form className="space-y-4" onSubmit={handleCreateAgent}>
<div className="space-y-2">
<Label htmlFor="base_url">{t('A2A 部署地址', 'A2A base URL')}</Label>
<Input
id="base_url"
value={agentForm.base_url}
onChange={(e) => setAgentForm((s) => ({ ...s, base_url: e.target.value }))}
placeholder={t('https://agent.example.com 或 agent.example.com:19090', 'https://agent.example.com or agent.example.com:19090')}
/>
<p className="text-xs text-muted-foreground leading-relaxed">
{t('默认只需要填写部署地址。保存时会自动读取', 'Usually the base URL is enough. Save will auto-read')}
<code className="mx-1">/.well-known</code>
{t('路径并补齐 card 信息。', 'and complete the card metadata.')}
</p>
</div>
<Collapsible open={agentAdvancedOpen} onOpenChange={setAgentAdvancedOpen}>
<CollapsibleTrigger asChild>
<Button type="button" variant="outline" className="w-full justify-between">
{t('高级设置(可选)', 'Advanced settings (optional)')}
<ChevronDown className={`w-4 h-4 transition-transform ${agentAdvancedOpen ? 'rotate-180' : ''}`} />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-4">
<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={agentForm.id} onChange={(e) => setAgentForm((s) => ({ ...s, id: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="name">{t('名称', 'Name')}</Label>
<Input id="name" value={agentForm.name} onChange={(e) => setAgentForm((s) => ({ ...s, name: e.target.value }))} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">{t('描述', 'Description')}</Label>
<Textarea id="description" value={agentForm.description} onChange={(e) => setAgentForm((s) => ({ ...s, description: e.target.value }))} rows={3} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="endpoint">{t('接口地址', 'Endpoint URL')}</Label>
<Input id="endpoint" value={agentForm.endpoint} onChange={(e) => setAgentForm((s) => ({ ...s, endpoint: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="card_url">{t('卡片地址', 'Card URL')}</Label>
<Input id="card_url" value={agentForm.card_url} onChange={(e) => setAgentForm((s) => ({ ...s, card_url: e.target.value }))} />
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="auth_mode">{t('鉴权模式', 'Auth mode')}</Label>
<select
id="auth_mode"
value={agentForm.auth_mode}
onChange={(e) => setAgentForm((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"
>
<option value="none">none</option>
<option value="oauth_backend_token">oauth_backend_token</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="auth_audience">Audience</Label>
<Input id="auth_audience" value={agentForm.auth_audience} onChange={(e) => setAgentForm((s) => ({ ...s, auth_audience: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="auth_scopes">Scopes</Label>
<Input id="auth_scopes" value={agentForm.auth_scopes} onChange={(e) => setAgentForm((s) => ({ ...s, auth_scopes: e.target.value }))} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="auth_env">{t('认证环境变量', 'Credential env var')}</Label>
<Input id="auth_env" value={agentForm.auth_env} onChange={(e) => setAgentForm((s) => ({ ...s, auth_env: e.target.value }))} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="tags">{t('标签', 'Tags')}</Label>
<Input id="tags" value={agentForm.tags} onChange={(e) => setAgentForm((s) => ({ ...s, tags: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="aliases">{t('别名', 'Aliases')}</Label>
<Input id="aliases" value={agentForm.aliases} onChange={(e) => setAgentForm((s) => ({ ...s, aliases: e.target.value }))} />
</div>
</div>
</CollapsibleContent>
</Collapsible>
<div className="rounded-md border border-border/70 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
{t(
'如果这是持久化本地 Sub-Agent请改用下面的 Sub-Agent 面板,不要在这里单独删除 registry 记录。',
'If this is a persistent local sub-agent, manage it from the sub-agent panel below instead of deleting the registry entry here.'
)}
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => handleAgentDialogOpenChange(false)}>
{t('取消', 'Cancel')}
</Button>
<Button type="submit" disabled={agentSubmitting}>
{agentSubmitting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Plus className="w-4 h-4 mr-2" />}
{t('保存', 'Save')}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
<Dialog open={subagentDialogOpen} onOpenChange={handleSubagentDialogOpenChange}>
<DialogTrigger asChild>
<Button size="sm">
<Plus className="w-4 h-4 mr-2" />
{t('新增 Sub-Agent', 'Add sub-agent')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
<DialogTitle>{editingSubagentId ? t('编辑 Sub-Agent', 'Edit sub-agent') : t('新增 Persistent Sub-Agent', 'Create persistent sub-agent')}</DialogTitle>
</DialogHeader>
<form className="space-y-4" onSubmit={handleSaveSubagent}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="subagent_id">ID</Label>
<Input
id="subagent_id"
value={subagentForm.id}
disabled={Boolean(editingSubagentId)}
onChange={(e) => setSubagentForm((s) => ({ ...s, id: e.target.value }))}
placeholder="research-agent"
/>
</div>
<div className="space-y-2">
<Label htmlFor="subagent_name">{t('名称', 'Name')}</Label>
<Input
id="subagent_name"
value={subagentForm.name}
onChange={(e) => setSubagentForm((s) => ({ ...s, name: e.target.value }))}
placeholder="Research Agent"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="subagent_description">{t('描述', 'Description')}</Label>
<Textarea
id="subagent_description"
rows={3}
value={subagentForm.description}
onChange={(e) => setSubagentForm((s) => ({ ...s, description: e.target.value }))}
placeholder={t('用于研究和资料整理的本地持久化子智能体', 'A persistent local sub-agent for research and note taking')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="subagent_prompt">System Prompt</Label>
<Textarea
id="subagent_prompt"
rows={6}
value={subagentForm.system_prompt}
onChange={(e) => setSubagentForm((s) => ({ ...s, system_prompt: e.target.value }))}
placeholder="Focus on research tasks, be concise, and cite concrete findings."
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="subagent_model">{t('模型', 'Model')}</Label>
<Input
id="subagent_model"
value={subagentForm.model}
onChange={(e) => setSubagentForm((s) => ({ ...s, model: e.target.value }))}
placeholder={t('留空则继承主 Agent 默认模型', 'Leave blank to inherit the lead agent model')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="delegation_mode">{t('委派模式', 'Delegation mode')}</Label>
<select
id="delegation_mode"
value={subagentForm.delegation_mode}
onChange={(e) => setSubagentForm((s) => ({ ...s, delegation_mode: e.target.value }))}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
>
<option value="remote_a2a_only">remote_a2a_only</option>
<option value="full">full</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex items-center justify-between rounded-md border border-border/70 px-3 py-2">
<div>
<Label htmlFor="subagent_enabled">{t('启用', 'Enabled')}</Label>
<p className="text-xs text-muted-foreground mt-1">{t('关闭后仍保留 workspace 和配置', 'Turning this off keeps the workspace and config intact')}</p>
</div>
<Switch
id="subagent_enabled"
checked={subagentForm.enabled}
onCheckedChange={(checked) => setSubagentForm((s) => ({ ...s, enabled: checked }))}
/>
</div>
<div className="flex items-center justify-between rounded-md border border-border/70 px-3 py-2">
<div>
<Label htmlFor="subagent_allow_mcp">{t('允许 MCP', 'Allow MCP')}</Label>
<p className="text-xs text-muted-foreground mt-1">{t('保留 MCP 配置并在运行时接入', 'Keep MCP config and attach it at runtime')}</p>
</div>
<Switch
id="subagent_allow_mcp"
checked={subagentForm.allow_mcp}
onCheckedChange={(checked) => setSubagentForm((s) => ({ ...s, allow_mcp: checked }))}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="subagent_tags">{t('标签', 'Tags')}</Label>
<Input
id="subagent_tags"
value={subagentForm.tags}
onChange={(e) => setSubagentForm((s) => ({ ...s, tags: e.target.value }))}
placeholder="research, notes"
/>
</div>
<div className="space-y-2">
<Label htmlFor="subagent_aliases">{t('别名', 'Aliases')}</Label>
<Input
id="subagent_aliases"
value={subagentForm.aliases}
onChange={(e) => setSubagentForm((s) => ({ ...s, aliases: e.target.value }))}
placeholder="researcher, local-research"
/>
</div>
</div>
<Collapsible open={subagentAdvancedOpen} onOpenChange={setSubagentAdvancedOpen}>
<CollapsibleTrigger asChild>
<Button type="button" variant="outline" className="w-full justify-between">
{t('原始 JSON 设置Metadata / MCP', 'Raw JSON settings (Metadata / MCP)')}
<ChevronDown className={`w-4 h-4 transition-transform ${subagentAdvancedOpen ? 'rotate-180' : ''}`} />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-4">
<div className="space-y-2">
<Label htmlFor="subagent_metadata_json">Metadata JSON</Label>
<Textarea
id="subagent_metadata_json"
rows={6}
value={subagentForm.metadata_json}
onChange={(e) => setSubagentForm((s) => ({ ...s, metadata_json: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="subagent_mcp_json">MCP Servers JSON</Label>
<Textarea
id="subagent_mcp_json"
rows={8}
value={subagentForm.mcp_servers_json}
onChange={(e) => setSubagentForm((s) => ({ ...s, mcp_servers_json: e.target.value }))}
/>
</div>
</CollapsibleContent>
</Collapsible>
<div className="rounded-md border border-border/70 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
{t('创建后会自动生成独立 workspace、写入', 'Creating this will generate an isolated workspace and write')}
<code className="mx-1">AGENTS.json</code>
{t('和', 'and')}
<code className="mx-1">AGENTS.md</code>
{t(',并注册到工作区智能体列表。', 'and register it in the workspace agent registry.')}
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => handleSubagentDialogOpenChange(false)}>
{t('取消', 'Cancel')}
</Button>
<Button type="submit" disabled={subagentSubmitting}>
{subagentSubmitting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : editingSubagentId ? <Pencil className="w-4 h-4 mr-2" /> : <Plus className="w-4 h-4 mr-2" />}
{editingSubagentId ? t('更新', 'Update') : t('创建', 'Create')}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
</div>
{error && (
<Card className="border-destructive">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-destructive text-sm">
<AlertCircle className="w-4 h-4" />
{error}
</div>
</CardContent>
</Card>
)}
<Tabs defaultValue="agents" className="space-y-4">
<TabsList>
<TabsTrigger value="agents">{t('委派目标', 'Delegation targets')}</TabsTrigger>
<TabsTrigger value="subagents">{t('Persistent Sub-Agents', 'Persistent sub-agents')}</TabsTrigger>
</TabsList>
<TabsContent value="agents" className="space-y-4">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{agents.map((agent) => {
const isWorkspace = agent.source === 'workspace';
const isManagedSubagent = Boolean(agent.metadata && agent.metadata.local_subagent);
return (
<Card key={agent.id}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<CardTitle className="text-base truncate">{agent.name}</CardTitle>
<p className="text-xs text-muted-foreground mt-1 font-mono">{agent.id}</p>
<p className="text-sm text-muted-foreground mt-2 leading-relaxed">
{agent.description || t('暂无描述', 'No description')}
</p>
</div>
<div className="flex items-center gap-2 flex-wrap justify-end">
<Badge variant="outline">{agentSourceLabel(agent.source, locale)}</Badge>
<Badge variant="secondary">{agent.protocol || t('本地', 'Local')}</Badge>
{isManagedSubagent && <Badge className="bg-amber-600">{t('受管 Sub-Agent', 'Managed sub-agent')}</Badge>}
{agent.support_streaming && <Badge className="bg-sky-600">{t('流式', 'Streaming')}</Badge>}
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 pt-0">
<div className="grid grid-cols-1 gap-2 text-xs text-muted-foreground">
{agent.base_url && <div><span className="font-medium text-foreground">{t('基础地址:', 'Base URL:')}</span> {agent.base_url}</div>}
{agent.endpoint && <div><span className="font-medium text-foreground">{t('接口地址:', 'Endpoint:')}</span> {agent.endpoint}</div>}
{agent.card_url && <div><span className="font-medium text-foreground">{t('卡片地址:', 'Card URL:')}</span> {agent.card_url}</div>}
{agent.auth_env && <div><span className="font-medium text-foreground">{t('认证变量:', 'Auth env:')}</span> {agent.auth_env}</div>}
{agent.auth_mode && agent.auth_mode !== 'none' && <div><span className="font-medium text-foreground">{t('鉴权模式:', 'Auth mode:')}</span> {agent.auth_mode}</div>}
{agent.auth_audience && <div><span className="font-medium text-foreground">Audience</span> {agent.auth_audience}</div>}
{(agent.auth_scopes || []).length > 0 && <div><span className="font-medium text-foreground">Scopes</span> {(agent.auth_scopes || []).join(', ')}</div>}
</div>
{(agent.tags.length > 0 || agent.aliases.length > 0) && (
<div className="space-y-2">
{agent.tags.length > 0 && (
<div className="flex items-start gap-2 flex-wrap">
<Tags className="w-3.5 h-3.5 mt-0.5 text-muted-foreground" />
{agent.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">{tag}</Badge>
))}
</div>
)}
{agent.aliases.length > 0 && (
<div className="flex items-center gap-2 flex-wrap text-xs text-muted-foreground">
<span className="font-medium text-foreground">{t('别名:', 'Aliases:')}</span>
{agent.aliases.map((alias) => (
<code key={alias} className="px-2 py-0.5 rounded bg-muted">{alias}</code>
))}
</div>
)}
</div>
)}
<div className="flex justify-end">
{isManagedSubagent ? (
<span className="text-xs text-muted-foreground">{t('请在 Sub-Agent 面板管理', 'Manage this in the sub-agent panel')}</span>
) : isWorkspace ? (
<Button variant="outline" size="sm" onClick={() => handleDeleteAgent(agent.id)}>
<Trash2 className="w-4 h-4 mr-2" />
{t('删除', 'Delete')}
</Button>
) : (
<span className="text-xs text-muted-foreground">{t('只读来源', 'Read-only source')}</span>
)}
</div>
</CardContent>
</Card>
);
})}
</div>
</TabsContent>
<TabsContent value="subagents" className="space-y-4">
<Card className="border-border/70 bg-muted/20">
<CardContent className="pt-6 text-sm text-muted-foreground leading-relaxed">
{t('持久化 Sub-Agent 会在', 'Persistent sub-agents keep their own workspace under')}
<code className="mx-1">~/.nanobot/workspace/agents/&lt;id&gt;_agent</code>
{t('下拥有自己的 workspace、`AGENTS.json`、`AGENTS.md`、skills 和 memory。默认委派模式是', ', plus `AGENTS.json`, `AGENTS.md`, skills, and memory. The default delegation mode is')}
<code className="mx-1">remote_a2a_only</code>
{t(',即只能向外委派到远端 A2A agent。', ', which only allows delegation to remote A2A agents.')}
</CardContent>
</Card>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{subagents.map((subagent) => (
<Card key={subagent.id}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<CardTitle className="text-base truncate">{subagent.name}</CardTitle>
<p className="text-xs text-muted-foreground mt-1 font-mono">{subagent.id}</p>
<p className="text-sm text-muted-foreground mt-2 leading-relaxed">
{subagent.description || t('暂无描述', 'No description')}
</p>
</div>
<div className="flex items-center gap-2 flex-wrap justify-end">
<Badge variant={subagent.enabled ? 'default' : 'outline'}>
{subagent.enabled ? t('启用', 'Enabled') : t('停用', 'Disabled')}
</Badge>
<Badge variant="secondary">{subagent.delegation_mode}</Badge>
{subagent.allow_mcp && <Badge className="bg-sky-600">MCP</Badge>}
{subagent.model && <Badge variant="outline">{subagent.model}</Badge>}
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 pt-0">
<div className="grid grid-cols-1 gap-2 text-xs text-muted-foreground">
<div><span className="font-medium text-foreground">{t('Workspace', 'Workspace:')}</span> {subagent.workspace}</div>
<div><span className="font-medium text-foreground">Base URL</span> {subagent.base_url}</div>
<div><span className="font-medium text-foreground">RPC</span> {subagent.endpoint}</div>
<div><span className="font-medium text-foreground">Card</span> {subagent.card_url}</div>
<div><span className="font-medium text-foreground">{t('MCP Servers', 'MCP servers:')}</span> {Object.keys(subagent.mcp_servers || {}).length}</div>
</div>
{subagent.system_prompt && (
<div className="rounded-md border border-border/70 bg-muted/30 px-3 py-2">
<div className="text-xs font-medium text-foreground mb-1">System Prompt</div>
<p className="text-xs text-muted-foreground whitespace-pre-wrap line-clamp-5">
{subagent.system_prompt}
</p>
</div>
)}
{(subagent.tags.length > 0 || subagent.aliases.length > 0) && (
<div className="space-y-2">
{subagent.tags.length > 0 && (
<div className="flex items-start gap-2 flex-wrap">
<Tags className="w-3.5 h-3.5 mt-0.5 text-muted-foreground" />
{subagent.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">{tag}</Badge>
))}
</div>
)}
{subagent.aliases.length > 0 && (
<div className="flex items-center gap-2 flex-wrap text-xs text-muted-foreground">
<span className="font-medium text-foreground">{t('别名:', 'Aliases:')}</span>
{subagent.aliases.map((alias) => (
<code key={alias} className="px-2 py-0.5 rounded bg-muted">{alias}</code>
))}
</div>
)}
</div>
)}
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={() => handleEditSubagent(subagent)}>
<Pencil className="w-4 h-4 mr-2" />
{t('编辑', 'Edit')}
</Button>
<Button variant="outline" size="sm" onClick={() => handleDeleteManagedSubagent(subagent.id)}>
<Trash2 className="w-4 h-4 mr-2" />
{t('删除', 'Delete')}
</Button>
</div>
</CardContent>
</Card>
))}
</div>
{subagents.length === 0 && (
<Card>
<CardContent className="pt-6 text-sm text-muted-foreground">
{t('还没有持久化 Sub-Agent。点击右上角“新增 Sub-Agent”开始创建。', 'There are no persistent sub-agents yet. Use "Add sub-agent" in the top-right corner to create one.')}
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
</div>
);
}