Files
steven_li ebfa242862 feat(outlook): 添加Outlook集成功能支持
添加完整的Outlook MCP集成,包括邮件和日历功能,通过AuthZ模式进行认证和权限管理,
支持邮箱连接、断开、状态检查和数据同步等功能。

fix(config): 统一配置文件路径从.nanobot到.beaver

将配置文件路径从/root/.nanobot统一更改为/root/.beaver,更新Dockerfile中的环境变量定义,
确保所有组件使用一致的配置目录结构。

feat(agent): 添加代理删除功能和助手身份提示

为代理注册表添加delete_agent方法,实现代理的动态删除功能;同时添加海狸助手身份提示,
确保AI助手在交互中保持一致的身份认知。

feat(engine): 增强引擎循环并添加意图决策快照

扩展AgentLoop类,添加intent_agent_decision参数用于意图驱动的代理决策,并在会话中记录
决策快照,便于后续分析和调试。

feat(authz): 扩展认证客户端功能

为AuthzClient添加设置权限、用户注册、后端注册和Outlook设置管理等新方法,增强系统
的认证和授权能力。
2026-05-14 16:01:46 +08:00

814 lines
40 KiB
TypeScript
Raw Permalink 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">~/.beaver/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>
);
}