代码提交
This commit is contained in:
1 parent
2688cda6b5
commit
a35aeee6a2
6 files changed
+294
-12
No files matched your search
@@ -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 }> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user