第一次提交

This commit is contained in:
2026-03-13 16:40:08 +08:00
commit 0a49bcfb2d
277 changed files with 61890 additions and 0 deletions

View File

@ -0,0 +1,363 @@
'use client';
import React, { useCallback, useEffect, useState } from 'react';
import { Bot, Plus, RefreshCw, Trash2, Loader2, AlertCircle, Tags, ChevronDown } from 'lucide-react';
import { addAgent, deleteAgent, listAgents, refreshAgents } from '@/lib/api';
import { useChatStore } from '@/lib/store';
import type { UiAgentDescriptor } 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 { Textarea } from '@/components/ui/textarea';
const EMPTY_FORM = {
id: '',
name: '',
description: '',
base_url: '',
endpoint: '',
card_url: '',
auth_env: '',
auth_mode: 'none',
auth_audience: '',
auth_scopes: '',
tags: '',
aliases: '',
};
export default function AgentsPage() {
const cachedAgents = useChatStore((s) => s.agentRegistry);
const setCachedAgents = useChatStore((s) => s.setAgentRegistry);
const [agents, setAgents] = useState<UiAgentDescriptor[]>(cachedAgents);
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 load = useCallback(async (background = false) => {
if (background) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);
try {
const data = await listAgents();
const nextAgents = Array.isArray(data) ? data : [];
setAgents(nextAgents);
setCachedAgents(nextAgents);
} catch (err: any) {
setError(err.message || '加载智能体失败');
} 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 data = await refreshAgents();
const nextAgents = data.agents || [];
setAgents(nextAgents);
setCachedAgents(nextAgents);
} catch (err: any) {
setError(err.message || '刷新智能体失败');
} finally {
setRefreshing(false);
}
};
const handleDialogOpenChange = (open: boolean) => {
setDialogOpen(open);
if (!open) {
setAdvancedOpen(false);
setForm(EMPTY_FORM);
}
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
const hasAddress = [form.base_url, form.endpoint, form.card_url].some((value) => value.trim());
if (!hasAddress) {
setError('请至少填写 A2A 部署地址、接口地址或卡片地址');
return;
}
setSubmitting(true);
setError(null);
try {
await addAgent({
id: form.id || undefined,
name: form.name || undefined,
description: form.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'
? []
: 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),
});
handleDialogOpenChange(false);
await load();
} catch (err: any) {
setError(err.message || '新增智能体失败');
} finally {
setSubmitting(false);
}
};
const handleDelete = async (agentId: string) => {
try {
await deleteAgent(agentId);
await load();
} catch (err: any) {
setError(err.message || '删除智能体失败');
}
};
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" />
</h1>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleRefresh}>
<RefreshCw className={`w-4 h-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
</Button>
<Dialog open={dialogOpen} onOpenChange={handleDialogOpenChange}>
<DialogTrigger asChild>
<Button size="sm">
<Plus className="w-4 h-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form className="space-y-4" onSubmit={handleCreate}>
<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 }))}
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
</p>
</div>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<CollapsibleTrigger asChild>
<Button type="button" variant="outline" className="w-full justify-between">
<ChevronDown className={`w-4 h-4 transition-transform ${advancedOpen ? '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 自动生成" />
</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 自动填充" />
</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 自动填充" />
</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" />
</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" />
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="auth_mode"></Label>
<select
id="auth_mode"
value={form.auth_mode}
onChange={(e) => setForm((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={form.auth_audience} onChange={(e) => setForm((s) => ({ ...s, auth_audience: e.target.value }))} placeholder="planner 或 a2a:planner" />
</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" />
</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" />
</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="例如:评审, 代码, 安全" />
</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" />
</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>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => handleDialogOpenChange(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>
</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>
)}
<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>
))}
</div>
)}
</div>
)}
<div className="flex justify-end">
{isWorkspace ? (
<Button variant="outline" size="sm" onClick={() => handleDelete(agent.id)}>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</div>
</CardContent>
</Card>
);
})}
</div>
</div>
);
}