智能体分权

This commit is contained in:
吉鑫升 committed 2026-05-13 11:36:17 +08:00
1 parent e7a455f22e
commit cf2e4bdd9c
31 files changed
+1237 -436

No files matched your search

@@ -1,8 +1,15 @@
"use client";
import { BotIcon, MessageSquareIcon, SquarePenIcon, Trash2Icon } from "lucide-react";
import {
BotIcon,
EyeIcon,
EyeOffIcon,
MessageSquareIcon,
SquarePenIcon,
Trash2Icon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
@@ -24,9 +31,11 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useDeleteAgent, useUpdateAgent } from "@/core/agents";
import type { Agent } from "@/core/agents";
import { getStoredAuth } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks";
import { useSkills } from "@/core/skills/hooks";
@@ -42,28 +51,45 @@ export function AgentCard({ agent }: AgentCardProps) {
const { skills, isLoading: isSkillsLoading } = useSkills();
const [deleteOpen, setDeleteOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const [editName, setEditName] = useState(agent.name);
const [editDescription, setEditDescription] = useState(agent.description ?? "");
const [editSoul, setEditSoul] = useState(agent.soul ?? "");
const [editPublished, setEditPublished] = useState(agent.published);
const [selectedSkills, setSelectedSkills] = useState<string[]>(
agent.skills ?? [],
);
const currentUserId = useMemo(() => getStoredAuth()?.user_id ?? null, []);
const isBuiltin = agent.builtin;
const isOwned =
!isBuiltin && agent.user_id !== null && agent.user_id === currentUserId;
const isFromOthers = !isBuiltin && !isOwned;
useEffect(() => {
if (!editOpen) {
return;
}
setEditName(agent.name);
setEditDescription(agent.description ?? "");
setEditSoul(agent.soul ?? "");
setEditPublished(agent.published);
setSelectedSkills(agent.skills ?? []);
}, [agent.description, agent.skills, agent.soul, editOpen]);
}, [
agent.description,
agent.name,
agent.published,
agent.skills,
agent.soul,
editOpen,
]);
function handleChat() {
router.push(`/workspace/agents/${agent.name}/chats/new`);
router.push(`/workspace/agents/${agent.id}/chats/new`);
}
async function handleDelete() {
try {
await deleteAgent.mutateAsync(agent.name);
await deleteAgent.mutateAsync(agent.id);
toast.success(t.agents.deleteSuccess);
setDeleteOpen(false);
} catch (err) {
@@ -72,12 +98,19 @@ export function AgentCard({ agent }: AgentCardProps) {
}
async function handleSaveEdit() {
const trimmedName = editName.trim();
if (!trimmedName) {
toast.error(t.agents.nameStepInvalidError ?? "名称不能为空");
return;
}
try {
await updateAgent.mutateAsync({
name: agent.name,
id: agent.id,
request: {
name: trimmedName,
description: editDescription,
soul: editSoul,
published: editPublished,
skills: selectedSkills,
},
});
@@ -88,6 +121,18 @@ export function AgentCard({ agent }: AgentCardProps) {
}
}
async function handleTogglePublished() {
try {
await updateAgent.mutateAsync({
id: agent.id,
request: { published: !agent.published },
});
toast.success(agent.published ? "已取消发布" : "已发布");
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
function toggleSkill(skillName: string) {
setSelectedSkills((prev) => {
if (prev.includes(skillName)) {
@@ -119,6 +164,39 @@ export function AgentCard({ agent }: AgentCardProps) {
)}
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
{isBuiltin && (
<Badge variant="secondary" className="text-xs">
内置
</Badge>
)}
{isOwned && (
<Badge
variant="default"
className="bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300 text-xs"
>
我的
</Badge>
)}
{isFromOthers && (
<Badge
variant="outline"
className="border-blue-500/30 text-blue-700 dark:text-blue-300 text-xs"
title={agent.user_id ? `来自用户 ${agent.user_id}` : "已发布"}
>
来自他人
</Badge>
)}
{isOwned && agent.published && (
<Badge
variant="outline"
className="border-sky-500/30 text-sky-700 dark:text-sky-300 text-xs"
title="该智能体已对所有用户可见"
>
已发布
</Badge>
)}
</div>
</div>
{agent.description && (
<CardDescription className="mt-2 line-clamp-2 text-sm">
@@ -157,41 +235,61 @@ export function AgentCard({ agent }: AgentCardProps) {
<MessageSquareIcon className="mr-1.5 h-3.5 w-3.5" />
{t.agents.chat}
</Button>
<div className="flex gap-1">
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
onClick={() => setEditOpen(true)}
title="编辑智能体"
>
<SquarePenIcon className="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
onClick={() => setDeleteOpen(true)}
title={t.agents.delete}
>
<Trash2Icon className="h-3.5 w-3.5" />
</Button>
</div>
{isOwned && (
<div className="flex gap-1">
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
onClick={() => void handleTogglePublished()}
disabled={updateAgent.isPending}
title={agent.published ? "取消发布" : "发布"}
>
{agent.published ? (
<EyeOffIcon className="h-3.5 w-3.5" />
) : (
<EyeIcon className="h-3.5 w-3.5" />
)}
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
onClick={() => setEditOpen(true)}
title="编辑智能体"
>
<SquarePenIcon className="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
onClick={() => setDeleteOpen(true)}
title={t.agents.delete}
>
<Trash2Icon className="h-3.5 w-3.5" />
</Button>
</div>
)}
</CardFooter>
</Card>
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>编辑智能体:{agent.name}</DialogTitle>
<DialogTitle>编辑智能体:{agent.name}</DialogTitle>
<DialogDescription>
可以修改描述、技能白名单和 SOUL 内容。
可以修改名称、描述、技能白名单、SOUL 内容与发布状态。
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<label className="text-sm font-medium">名称</label>
<Input value={agent.name} disabled />
<Input
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="输入智能体名称(允许中文)"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">描述</label>
@@ -238,7 +336,7 @@ export function AgentCard({ agent }: AgentCardProps) {
)}
</div>
<p className="text-muted-foreground text-xs">
不选任何技能会保存为无技能(skills = [])。
不选任何技能会保存为无技能(skills = [])。
</p>
</div>
<div className="space-y-2">
@@ -250,6 +348,18 @@ export function AgentCard({ agent }: AgentCardProps) {
className="min-h-40 font-mono text-xs"
/>
</div>
<div className="flex items-center justify-between rounded-md border p-3">
<div>
<p className="text-sm font-medium">公开发布</p>
<p className="text-muted-foreground text-xs">
打开后,该智能体对所有用户可见。
</p>
</div>
<Switch
checked={editPublished}
onCheckedChange={setEditPublished}
/>
</div>
</div>
<DialogFooter>
<Button
@@ -59,7 +59,7 @@ export function AgentGallery() {
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{agents.map((agent) => (
<AgentCard key={agent.name} agent={agent} />
<AgentCard key={agent.id} agent={agent} />
))}
</div>
)}
@@ -62,10 +62,10 @@ export function RecentChatList() {
const { t } = useI18n();
const router = useRouter();
const pathname = usePathname();
const { thread_id: threadIdFromPath, agent_name: agentNameFromPath } =
const { thread_id: threadIdFromPath, agent_id: agentIdFromPath } =
useParams<{
thread_id: string;
agent_name?: string;
agent_id?: string;
}>();
const { data: threads = [] } = useThreads();
const { mutate: deleteThread } = useDeleteThread();
@@ -82,7 +82,7 @@ export function RecentChatList() {
if (threadId === threadIdFromPath) {
const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
let nextThreadPath = pathOfThread("new", {
agent_name: agentNameFromPath,
agent_id: agentIdFromPath,
});
if (threadIndex > -1) {
if (threads[threadIndex + 1]) {
@@ -94,7 +94,7 @@ export function RecentChatList() {
void router.push(nextThreadPath);
}
},
[agentNameFromPath, deleteThread, router, threadIdFromPath, threads],
[agentIdFromPath, deleteThread, router, threadIdFromPath, threads],
);
const handleRenameClick = useCallback(