From bf023d746abef304be7db5a0e18c00887138a47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Wed, 12 Aug 2026 15:05:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=AF?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=86=85=E7=BD=AE=E6=8A=80=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8A=80=E8=83=BD=E5=86=85=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增内置技能删除与技能文件删除 API,允许管理员编辑/删除内置技能文件;技能设置页与文件预览弹窗同步支持删除操作。 Co-authored-by: Cursor --- .../settings/skill-settings-page.tsx | 65 ++--- .../skills/skill-file-preview-dialog.tsx | 29 +++ frontend-web/src/core/i18n/locales/en-US.ts | 11 +- frontend-web/src/core/i18n/locales/types.ts | 11 +- frontend-web/src/core/i18n/locales/zh-CN.ts | 7 +- frontend-web/src/core/skills/api.ts | 38 ++- frontend-web/src/core/skills/hooks.ts | 40 ++- .../backend/app/gateway/routers/skills.py | 231 ++++++++++++++++-- .../packages/harness/deerflow/skills/types.py | 2 +- 9 files changed, 361 insertions(+), 73 deletions(-) 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 && ( <>