代码提交

This commit is contained in:
吉鑫升 authored and xuhd committed 2026-05-28 18:28:57 +08:00
1 parent 2688cda6b5
commit a35aeee6a2
6 files changed
+294 -12

No files matched your search

+25
View File
@@ -174,6 +174,31 @@ export async function setAgentFeatured(
return res.json() as Promise<Agent>;
}
export interface AgentSkillsSyncRemoval {
agent_id: string;
agent_name: string | null;
user_id: string | null;
removed_skills: string[];
}
export interface AgentSkillsSyncResult {
checked: number;
modified: number;
removals: AgentSkillsSyncRemoval[];
}
export async function adminSyncAgentSkills(): Promise<AgentSkillsSyncResult> {
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents/admin/sync-skills`,
{ method: "POST" },
);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to sync agent skills: ${res.statusText}`);
}
return res.json() as Promise<AgentSkillsSyncResult>;
}
export async function checkAgentName(
name: string,
): Promise<{ available: boolean; name: string }> {
+13
View File
@@ -6,6 +6,7 @@ import {
} from "@tanstack/react-query";
import {
adminSyncAgentSkills,
createAgent,
deleteAgent,
getAgent,
@@ -180,3 +181,15 @@ export function useSetAgentPrivate() {
},
});
}
export function useAdminSyncAgentSkills() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: adminSyncAgentSkills,
onSuccess: (result) => {
if (result.modified > 0) {
invalidateAgentQueries(queryClient);
}
},
});
}
+8
View File
@@ -162,6 +162,10 @@ export function useSetSkillPublished() {
}) => setSkillPublished(skillName, published),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
// Unpublishing triggers the backend cascade that strips this skill
// from every referencing agent's config.yaml, so the cached agent
// list (agent.skills) is now stale.
void queryClient.invalidateQueries({ queryKey: ["agents"] });
},
});
}
@@ -194,6 +198,10 @@ export function useDeleteCustomSkill() {
mutationFn: (skillName: string) => deleteCustomSkill(skillName),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
// Deletion triggers the backend cascade that strips this skill from
// every referencing agent's config.yaml, so the cached agent list
// (agent.skills) is now stale.
void queryClient.invalidateQueries({ queryKey: ["agents"] });
},
});
}
+111 -10
View File
@@ -1,8 +1,18 @@
"use client";
import { BotIcon, SearchIcon } from "lucide-react";
import { BotIcon, RefreshCwIcon, SearchIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
WorkspaceBody,
@@ -10,12 +20,35 @@ import {
WorkspaceHeader,
} from "@/components/workspace/workspace-container";
import { AgentCard } from "@/components/workspace/agents/agent-card";
import { useAllAgentsAdmin } from "@/core/agents";
import { useAdminSyncAgentSkills, useAllAgentsAdmin } from "@/core/agents";
import type { AgentSkillsSyncResult } from "@/core/agents/api";
import { desensitize } from "@/lib/desensitize";
export default function AdminAgentsPage() {
const [search, setSearch] = useState("");
const { agents, isLoading } = useAllAgentsAdmin(true);
const syncSkills = useAdminSyncAgentSkills();
const [syncResult, setSyncResult] = useState<AgentSkillsSyncResult | null>(
null,
);
const [syncDialogOpen, setSyncDialogOpen] = useState(false);
async function handleSyncSkills() {
try {
const result = await syncSkills.mutateAsync();
setSyncResult(result);
setSyncDialogOpen(true);
if (result.modified === 0) {
toast.success("所有智能体的技能均有效,无需清理");
} else {
toast.success(
`已清理 ${result.modified} 个智能体中的失效技能引用`,
);
}
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
const filteredAgents = useMemo(() => {
const needle = search.trim().toLowerCase();
@@ -37,14 +70,28 @@ export default function AdminAgentsPage() {
管理员视图:查看、编辑、删除所有用户的智能体(包括内置、用户自定义与私密广场条目)。
</p>
</div>
<div className="relative w-64 shrink-0">
<SearchIcon className="text-muted-foreground absolute left-2.5 top-1/2 size-4 -translate-y-1/2" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="按名称搜索"
className="pl-8"
/>
<div className="flex shrink-0 items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleSyncSkills}
disabled={syncSkills.isPending}
title="扫描所有智能体的 config.yaml,移除指向已不存在技能的引用"
>
<RefreshCwIcon
className={`mr-1.5 size-4 ${syncSkills.isPending ? "animate-spin" : ""}`}
/>
{syncSkills.isPending ? "正在同步…" : "同步技能"}
</Button>
<div className="relative w-64">
<SearchIcon className="text-muted-foreground absolute left-2.5 top-1/2 size-4 -translate-y-1/2" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="按名称搜索"
className="pl-8"
/>
</div>
</div>
</div>
@@ -77,6 +124,60 @@ export default function AdminAgentsPage() {
</div>
</div>
</WorkspaceBody>
<Dialog open={syncDialogOpen} onOpenChange={setSyncDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>同步技能 — 清理结果</DialogTitle>
<DialogDescription>
{syncResult
? `扫描 ${syncResult.checked} 个智能体配置,清理 ${syncResult.modified} 个。`
: ""}
</DialogDescription>
</DialogHeader>
{syncResult && syncResult.removals.length > 0 ? (
<div className="max-h-[60vh] overflow-y-auto">
<ul className="space-y-3 text-sm">
{syncResult.removals.map((entry) => (
<li
key={entry.agent_id}
className="rounded-md border px-3 py-2"
>
<div className="font-medium">
{entry.agent_name
? desensitize(entry.agent_name)
: entry.agent_id}
</div>
<div className="text-muted-foreground mt-0.5 text-xs">
id: {entry.agent_id}
{entry.user_id ? ` · owner: ${entry.user_id}` : " · built-in"}
</div>
<div className="mt-1.5 flex flex-wrap gap-1.5">
{entry.removed_skills.map((s) => (
<span
key={s}
className="bg-muted rounded px-1.5 py-0.5 text-xs"
>
{s}
</span>
))}
</div>
</li>
))}
</ul>
</div>
) : (
<p className="text-muted-foreground text-sm">
所有智能体引用的技能均存在,未做任何修改。
</p>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSyncDialogOpen(false)}>
关闭
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</WorkspaceContainer>
);
}