feat(agent): 添加对持久化子智能体的支持并增强委派管理

添加了持久化子智能体的完整生命周期管理功能,包括创建、更新、删除和查询API接口。
新增了子智能体的JSON-RPC通信协议支持,实现了远程调用和任务管理功能。

同时增强了委派管理器的功能:
- 添加了对本地委派、插件委派和本地回退的开关控制
- 实现了持久化子智能体任务的自动检测和本地执行保护
- 增加了对不同委派类型的权限验证机制

修改了智能体注册表以支持插件智能体的条件性包含,并更新了工具注册逻辑以支持可选工具。

BREAKING CHANGE: 委派管理器的构造函数签名已更改,添加了新的控制参数。
```
This commit is contained in:
2026-03-27 10:15:35 +08:00
parent bad1e16ab4
commit 29dfd14aa6
133 changed files with 11656 additions and 220 deletions

View File

@ -1,11 +1,30 @@
'use client';
import React, { useCallback, useEffect, useState } from 'react';
import { Bot, Plus, RefreshCw, Trash2, Loader2, AlertCircle, Tags, ChevronDown } from 'lucide-react';
import {
AlertCircle,
Bot,
ChevronDown,
Loader2,
Pencil,
Plus,
RefreshCw,
Tags,
Trash2,
} from 'lucide-react';
import { addAgent, deleteAgent, listAgents, refreshAgents } from '@/lib/api';
import {
addAgent,
createSubagent,
deleteAgent,
deleteSubagent,
listAgents,
listSubagents,
refreshAgents,
updateSubagent,
} from '@/lib/api';
import { useChatStore } from '@/lib/store';
import type { UiAgentDescriptor } from '@/types';
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';
@ -13,9 +32,11 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
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';
const EMPTY_FORM = {
const EMPTY_AGENT_FORM = {
id: '',
name: '',
description: '',
@ -30,17 +51,69 @@ const EMPTY_FORM = {
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): Record<string, unknown> {
const probe = raw.trim();
if (!probe) {
return {};
}
let parsed: unknown;
try {
parsed = JSON.parse(probe);
} catch {
throw new Error(`${label} 需要是合法 JSON`);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${label} 需要是 JSON 对象`);
}
return parsed as Record<string, unknown>;
}
function parseNestedJsonObject(raw: string, label: string): Record<string, Record<string, unknown>> {
const parsed = parseJsonObject(raw, label);
for (const [key, value] of Object.entries(parsed)) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} 中的 ${key} 必须是 JSON 对象`);
}
}
return parsed as Record<string, Record<string, unknown>>;
}
export default function AgentsPage() {
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 [dialogOpen, setDialogOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [advancedOpen, setAdvancedOpen] = useState(false);
const [form, setForm] = useState(EMPTY_FORM);
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) {
@ -50,9 +123,14 @@ export default function AgentsPage() {
}
setError(null);
try {
const data = await listAgents();
const nextAgents = Array.isArray(data) ? data : [];
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 || '加载智能体失败');
@ -73,9 +151,14 @@ export default function AgentsPage() {
setError(null);
setRefreshing(true);
try {
const data = await refreshAgents();
const nextAgents = data.agents || [];
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 || '刷新智能体失败');
@ -84,59 +167,133 @@ export default function AgentsPage() {
}
};
const handleDialogOpenChange = (open: boolean) => {
setDialogOpen(open);
const handleAgentDialogOpenChange = (open: boolean) => {
setAgentDialogOpen(open);
if (!open) {
setAdvancedOpen(false);
setForm(EMPTY_FORM);
setAgentAdvancedOpen(false);
setAgentForm(EMPTY_AGENT_FORM);
}
};
const handleCreate = async (e: React.FormEvent) => {
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 = [form.base_url, form.endpoint, form.card_url].some((value) => value.trim());
const hasAddress = [agentForm.base_url, agentForm.endpoint, agentForm.card_url].some((value) => value.trim());
if (!hasAddress) {
setError('请至少填写 A2A 部署地址、接口地址或卡片地址');
return;
}
setSubmitting(true);
setAgentSubmitting(true);
setError(null);
try {
await addAgent({
id: form.id || undefined,
name: form.name || undefined,
description: form.description || undefined,
id: agentForm.id || undefined,
name: agentForm.name || undefined,
description: agentForm.description || undefined,
protocol: 'a2a',
base_url: form.base_url || undefined,
endpoint: form.endpoint || undefined,
card_url: form.card_url || undefined,
auth_env: form.auth_env || undefined,
auth_mode: form.auth_mode || 'none',
auth_audience: form.auth_mode === 'none' ? undefined : form.auth_audience || undefined,
auth_scopes: form.auth_mode === 'none'
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'
? []
: form.auth_scopes.split(',').map((item) => item.trim()).filter(Boolean),
tags: form.tags.split(',').map((item) => item.trim()).filter(Boolean),
aliases: form.aliases.split(',').map((item) => item.trim()).filter(Boolean),
: 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),
});
handleDialogOpenChange(false);
await load();
handleAgentDialogOpenChange(false);
await load(true);
} catch (err: any) {
setError(err.message || '新增智能体失败');
} finally {
setSubmitting(false);
setAgentSubmitting(false);
}
};
const handleDelete = async (agentId: string) => {
const handleDeleteAgent = async (agentId: string) => {
try {
await deleteAgent(agentId);
await load();
await load(true);
} catch (err: any) {
setError(err.message || '删除智能体失败');
}
};
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('Sub-agent ID 不能为空');
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'),
mcp_servers: parseNestedJsonObject(subagentForm.mcp_servers_json, 'MCP Servers'),
};
if (editingSubagentId) {
await updateSubagent(editingSubagentId, payload);
} else {
await createSubagent(payload);
}
handleSubagentDialogOpenChange(false);
await load(true);
} catch (err: any) {
setError(err.message || '保存 Sub-Agent 失败');
} finally {
setSubagentSubmitting(false);
}
};
const handleDeleteManagedSubagent = async (subagentId: string) => {
try {
await deleteSubagent(subagentId);
await load(true);
} catch (err: any) {
setError(err.message || '删除 Sub-Agent 失败');
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
@ -154,17 +311,17 @@ export default function AgentsPage() {
</h1>
<p className="text-sm text-muted-foreground mt-1">
A2A Sub-Agent
</p>
</div>
<div className="flex items-center gap-2">
<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' : ''}`} />
</Button>
<Dialog open={dialogOpen} onOpenChange={handleDialogOpenChange}>
<Dialog open={agentDialogOpen} onOpenChange={handleAgentDialogOpenChange}>
<DialogTrigger asChild>
<Button size="sm">
<Button size="sm" variant="outline">
<Plus className="w-4 h-4 mr-2" />
</Button>
@ -173,53 +330,51 @@ export default function AgentsPage() {
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form className="space-y-4" onSubmit={handleCreate}>
<form className="space-y-4" onSubmit={handleCreateAgent}>
<div className="space-y-2">
<Label htmlFor="base_url">A2A </Label>
<Input
id="base_url"
value={form.base_url}
onChange={(e) => setForm((s) => ({ ...s, base_url: e.target.value }))}
value={agentForm.base_url}
onChange={(e) => setAgentForm((s) => ({ ...s, base_url: e.target.value }))}
placeholder="https://agent.example.com 或 agent.example.com:19090"
/>
<p className="text-xs text-muted-foreground leading-relaxed">
<code className="mx-1">/.well-known/agent-card</code>
<code className="mx-1">/.well-known/agent-card.json</code>
<code className="mx-1">/.well-known/agent.json</code>
ID
<code className="mx-1">/.well-known</code>
card
</p>
</div>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<Collapsible open={agentAdvancedOpen} onOpenChange={setAgentAdvancedOpen}>
<CollapsibleTrigger asChild>
<Button type="button" variant="outline" className="w-full justify-between">
<ChevronDown className={`w-4 h-4 transition-transform ${advancedOpen ? 'rotate-180' : ''}`} />
<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={form.id} onChange={(e) => setForm((s) => ({ ...s, id: e.target.value }))} placeholder="留空则从 A2A card 自动生成" />
<Input id="id" value={agentForm.id} onChange={(e) => setAgentForm((s) => ({ ...s, id: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input id="name" value={form.name} onChange={(e) => setForm((s) => ({ ...s, name: e.target.value }))} placeholder="留空则从 A2A card 自动填充" />
<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"></Label>
<Textarea id="description" value={form.description} onChange={(e) => setForm((s) => ({ ...s, description: e.target.value }))} rows={3} placeholder="留空则从 A2A card 自动填充" />
<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"></Label>
<Input id="endpoint" value={form.endpoint} onChange={(e) => setForm((s) => ({ ...s, endpoint: e.target.value }))} placeholder="https://agent.example.com/rpc" />
<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"></Label>
<Input id="card_url" value={form.card_url} onChange={(e) => setForm((s) => ({ ...s, card_url: e.target.value }))} placeholder="https://agent.example.com/.well-known/agent-card" />
<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">
@ -227,8 +382,8 @@ export default function AgentsPage() {
<Label htmlFor="auth_mode"></Label>
<select
id="auth_mode"
value={form.auth_mode}
onChange={(e) => setForm((s) => ({ ...s, auth_mode: e.target.value }))}
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>
@ -237,46 +392,211 @@ export default function AgentsPage() {
</div>
<div className="space-y-2">
<Label htmlFor="auth_audience">Audience</Label>
<Input id="auth_audience" value={form.auth_audience} onChange={(e) => setForm((s) => ({ ...s, auth_audience: e.target.value }))} placeholder="planner 或 a2a:planner" />
<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={form.auth_scopes} onChange={(e) => setForm((s) => ({ ...s, auth_scopes: e.target.value }))} placeholder="run_task" />
<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"></Label>
<Input id="auth_env" value={form.auth_env} onChange={(e) => setForm((s) => ({ ...s, auth_env: e.target.value }))} placeholder="例如MY_AGENT_TOKEN" />
<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"></Label>
<Input id="tags" value={form.tags} onChange={(e) => setForm((s) => ({ ...s, tags: e.target.value }))} placeholder="例如:评审, 代码, 安全" />
<Input id="tags" value={agentForm.tags} onChange={(e) => setAgentForm((s) => ({ ...s, tags: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="aliases"></Label>
<Input id="aliases" value={form.aliases} onChange={(e) => setForm((s) => ({ ...s, aliases: e.target.value }))} placeholder="例如reviewer, audit-agent" />
<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">
card
<code className="mx-1">.well-known</code>
Sub-Agent Sub-Agent registry
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => handleDialogOpenChange(false)}>
<Button type="button" variant="outline" onClick={() => handleAgentDialogOpenChange(false)}>
</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" />}
<Button type="submit" disabled={agentSubmitting}>
{agentSubmitting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Plus className="w-4 h-4 mr-2" />}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
<Dialog open={subagentDialogOpen} onOpenChange={handleSubagentDialogOpenChange}>
<DialogTrigger asChild>
<Button size="sm">
<Plus className="w-4 h-4 mr-2" />
Sub-Agent
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
<DialogTitle>{editingSubagentId ? '编辑 Sub-Agent' : '新增 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"></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"></Label>
<Textarea
id="subagent_description"
rows={3}
value={subagentForm.description}
onChange={(e) => setSubagentForm((s) => ({ ...s, description: e.target.value }))}
placeholder="用于研究和资料整理的本地持久化子智能体"
/>
</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"></Label>
<Input
id="subagent_model"
value={subagentForm.model}
onChange={(e) => setSubagentForm((s) => ({ ...s, model: e.target.value }))}
placeholder="留空则继承主 Agent 默认模型"
/>
</div>
<div className="space-y-2">
<Label htmlFor="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"></Label>
<p className="text-xs text-muted-foreground mt-1"> workspace </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"> MCP</Label>
<p className="text-xs text-muted-foreground mt-1"> MCP </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"></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"></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">
JSON 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">
workspace
<code className="mx-1">AGENTS.json</code>
<code className="mx-1">AGENTS.md</code>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => handleSubagentDialogOpenChange(false)}>
</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 ? '更新' : '创建'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
</div>
@ -291,73 +611,178 @@ export default function AgentsPage() {
</Card>
)}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{agents.map((agent) => {
const isWorkspace = agent.source === 'workspace';
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 || '—'}
</p>
</div>
<div className="flex items-center gap-2 flex-wrap justify-end">
<Badge variant="outline">{agent.source === 'workspace' ? '工作区' : agent.source === 'plugin' ? '插件' : agent.source === 'skill' ? '技能' : '内置'}</Badge>
<Badge variant="secondary">{agent.protocol || '本地'}</Badge>
{agent.support_streaming && <Badge className="bg-sky-600"></Badge>}
{agent.support_group && <Badge className="bg-emerald-600"></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"></span> {agent.base_url}</div>}
{agent.endpoint && <div><span className="font-medium text-foreground"></span> {agent.endpoint}</div>}
{agent.card_url && <div><span className="font-medium text-foreground"></span> {agent.card_url}</div>}
{agent.auth_env && <div><span className="font-medium text-foreground"></span> {agent.auth_env}</div>}
{agent.auth_mode && agent.auth_mode !== 'none' && <div><span className="font-medium text-foreground"></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"></span>
{agent.aliases.map((alias) => (
<code key={alias} className="px-2 py-0.5 rounded bg-muted">{alias}</code>
))}
<Tabs defaultValue="agents" className="space-y-4">
<TabsList>
<TabsTrigger value="agents"></TabsTrigger>
<TabsTrigger value="subagents">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 || '—'}
</p>
</div>
<div className="flex items-center gap-2 flex-wrap justify-end">
<Badge variant="outline">{agent.source === 'workspace' ? '工作区' : agent.source === 'plugin' ? '插件' : agent.source === 'skill' ? '技能' : '内置'}</Badge>
<Badge variant="secondary">{agent.protocol || '本地'}</Badge>
{isManagedSubagent && <Badge className="bg-amber-600"> Sub-Agent</Badge>}
{agent.support_streaming && <Badge className="bg-sky-600"></Badge>}
{agent.support_group && <Badge className="bg-emerald-600"></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"></span> {agent.base_url}</div>}
{agent.endpoint && <div><span className="font-medium text-foreground"></span> {agent.endpoint}</div>}
{agent.card_url && <div><span className="font-medium text-foreground"></span> {agent.card_url}</div>}
{agent.auth_env && <div><span className="font-medium text-foreground"></span> {agent.auth_env}</div>}
{agent.auth_mode && agent.auth_mode !== 'none' && <div><span className="font-medium text-foreground"></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"></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"> Sub-Agent </span>
) : isWorkspace ? (
<Button variant="outline" size="sm" onClick={() => handleDeleteAgent(agent.id)}>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
) : (
<span className="text-xs text-muted-foreground"></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">
Sub-Agent
<code className="mx-1">~/.nanobot/workspace/agents/&lt;id&gt;_agent</code>
workspace`AGENTS.json``AGENTS.md`skills memory
<code className="mx-1">remote_a2a_only</code>
A2A agent
</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 || '—'}
</p>
</div>
<div className="flex items-center gap-2 flex-wrap justify-end">
<Badge variant={subagent.enabled ? 'default' : 'outline'}>
{subagent.enabled ? '启用' : '停用'}
</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>
)}
<div className="flex justify-end">
{isWorkspace ? (
<Button variant="outline" size="sm" onClick={() => handleDelete(agent.id)}>
</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">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">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"></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" />
</Button>
<Button variant="outline" size="sm" onClick={() => handleDeleteManagedSubagent(subagent.id)}>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
{subagents.length === 0 && (
<Card>
<CardContent className="pt-6 text-sm text-muted-foreground">
Sub-Agent Sub-Agent
</CardContent>
</Card>
);
})}
</div>
)}
</TabsContent>
</Tabs>
</div>
);
}