'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 { return JSON.stringify(value, null, 2); } function parseJsonObject(raw: string, label: string, locale: AppLocale): Record { 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; } function parseNestedJsonObject(raw: string, label: string, locale: AppLocale): Record> { 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>; } 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(cachedAgents); const [subagents, setSubagents] = useState([]); const [loading, setLoading] = useState(cachedAgents.length === 0); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(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(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 (
); } return (

{t('智能体', 'Agents')}

{t('管理外部 A2A 智能体,以及持久化的本地 Sub-Agent。', 'Manage external A2A agents and persistent local sub-agents.')}

{t('新增工作区智能体', 'Add workspace agent')}
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')} />

{t('默认只需要填写部署地址。保存时会自动读取', 'Usually the base URL is enough. Save will auto-read')} /.well-known {t('路径并补齐 card 信息。', 'and complete the card metadata.')}

setAgentForm((s) => ({ ...s, id: e.target.value }))} />
setAgentForm((s) => ({ ...s, name: e.target.value }))} />