diff --git a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx index 4f6a3e7d..42f92a24 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -71,8 +71,9 @@ import { useTheme } from "@/contexts/ThemeContext"; import { getStoredAuth } from "@/core/auth"; import { useI18n } from "@/core/i18n/hooks"; import type { ValidateSkillUploadResponse } from "@/core/skills/api"; -import { - useDeleteCustomSkill, +import { + useDeleteBuiltinSkill, + useDeleteCustomSkill, useDuplicateSkill, useGenerateMissingSkillNameZh, useGenerateSkillNameZh, @@ -616,9 +617,10 @@ export function SkillRow({ const { data: cardPublishSquares } = usePublishSquares(); const setPinned = useSetSkillPinned(); const setFeatured = useSetSkillFeatured(); - const setAlwaysOn = useSetSkillAlwaysOn(); - const { data: compression } = useSkillCompressionSettings(); - const deleteSkill = useDeleteCustomSkill(); + const setAlwaysOn = useSetSkillAlwaysOn(); + const { data: compression } = useSkillCompressionSettings(); + const deleteCustom = useDeleteCustomSkill(); + const deleteBuiltin = useDeleteBuiltinSkill(); const duplicateSkillMutation = useDuplicateSkill(); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [tagPickerOpen, setTagPickerOpen] = useState(false); @@ -634,8 +636,9 @@ export function SkillRow({ const isOwned = isCustom && !isBuiltin && !!skill.owner_user_id && skill.owner_user_id === currentUserId; const isFromOthers = isCustom && !isBuiltin && !isOwned; - // Admin can edit/delete/unpublish anyone's skill; non-admin only their own. - const canManage = isCustom && (isOwned || isAdmin); + // Admin can manage custom skills from any owner; non-admin only their own. + const canManage = isCustom && (isOwned || isAdmin); + const canDelete = isAdmin || isOwned; const canTogglePublish = canManage && !isBuiltin; // 常驻(免压缩):内置技能仅管理员可设;自定义技能其所有者或管理员可设(与后端一致)。 const canSetAlwaysOn = isAdmin || isOwned; @@ -686,11 +689,15 @@ export function SkillRow({ } } - async function handleDelete() { - try { - await deleteSkill.mutateAsync(skill.name); - toast.success(t.settings.skills.deleteSuccess); - setConfirmDeleteOpen(false); + async function handleDelete() { + try { + if (isBuiltin) { + await deleteBuiltin.mutateAsync(skill.name); + } else { + await deleteCustom.mutateAsync(skill.name); + } + toast.success(t.settings.skills.deleteSuccess); + setConfirmDeleteOpen(false); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } @@ -918,7 +925,7 @@ export function SkillRow({ > - {canManage && ( + {canManage && ( )} - {canManage && ( - - {canManage && ( - diff --git a/frontend-web/src/components/workspace/skills/skill-file-preview-dialog.tsx b/frontend-web/src/components/workspace/skills/skill-file-preview-dialog.tsx index 000aa9ae..d9a75f45 100644 --- a/frontend-web/src/components/workspace/skills/skill-file-preview-dialog.tsx +++ b/frontend-web/src/components/workspace/skills/skill-file-preview-dialog.tsx @@ -5,6 +5,7 @@ import { DownloadIcon, Loader2, PencilIcon, + Trash2Icon, XIcon, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; @@ -22,6 +23,7 @@ import { Textarea } from "@/components/ui/textarea"; import { MarkdownContent } from "@/components/workspace/messages/markdown-content"; import { getSkillFileDownloadURL } from "@/core/skills/api"; import { + useDeleteSkillFile, useSaveSkillFile, useSkillFileContent, } from "@/core/skills/hooks"; @@ -66,6 +68,7 @@ export function SkillFilePreviewDialog({ open ? path : null, ); const save = useSaveSkillFile(); + const remove = useDeleteSkillFile(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(""); @@ -77,6 +80,8 @@ export function SkillFilePreviewDialog({ }, [path, open]); const fileEditable = !!data?.editable && treeEditable; + const isRootSkillFile = path?.replace(/\\/g, "/").toLowerCase() === "skill.md"; + const fileDeletable = !!data && treeEditable && !isRootSkillFile; const isMarkdown = useMemo(() => { if (!path) return false; const lower = path.toLowerCase(); @@ -115,6 +120,18 @@ export function SkillFilePreviewDialog({ } } + async function handleDelete() { + if (!skillName || !path) return; + if (!window.confirm(tx.filesDeleteConfirm.replace("{path}", path))) return; + try { + await remove.mutateAsync({ skillName, path }); + toast.success(tx.filesDeleteSuccess); + onOpenChange(false); + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)); + } + } + async function handleCopyPath() { if (!path) return; await navigator.clipboard.writeText(path); @@ -158,6 +175,18 @@ export function SkillFilePreviewDialog({ {tx.filesEditAction} )} + {fileDeletable && !editing && ( + + )} {editing && ( <>