diff --git a/locales/en-US/setting.json b/locales/en-US/setting.json index 6108f08ce8..ed1bcda253 100644 --- a/locales/en-US/setting.json +++ b/locales/en-US/setting.json @@ -1178,6 +1178,9 @@ "tools.klavis.disconnect": "Disconnect", "tools.klavis.disconnected": "Disconnected", "tools.klavis.error": "Error", + "tools.klavis.remove": "Remove", + "tools.klavis.removeConfirm.desc": "{{name}} will be permanently removed from your connected services. This action cannot be undone.", + "tools.klavis.removeConfirm.title": "Remove {{name}}?", "tools.klavis.groupName": "Klavis Tools", "tools.klavis.manage": "Manage Klavis", "tools.klavis.manageTitle": "Manage Klavis Integration", diff --git a/locales/zh-CN/setting.json b/locales/zh-CN/setting.json index bc981f0f7c..a6efbcd6a3 100644 --- a/locales/zh-CN/setting.json +++ b/locales/zh-CN/setting.json @@ -1178,6 +1178,9 @@ "tools.klavis.disconnect": "断开连接", "tools.klavis.disconnected": "已断开连接", "tools.klavis.error": "错误", + "tools.klavis.remove": "移除", + "tools.klavis.removeConfirm.desc": "{{name}} 将从您的已连接服务中永久移除,此操作不可撤销。", + "tools.klavis.removeConfirm.title": "移除 {{name}}?", "tools.klavis.groupName": "Klavis 工具", "tools.klavis.manage": "管理 Klavis", "tools.klavis.manageTitle": "管理 Klavis 集成", diff --git a/packages/locales/src/default/setting.ts b/packages/locales/src/default/setting.ts index 13bcb8f70a..3cc67b74a5 100644 --- a/packages/locales/src/default/setting.ts +++ b/packages/locales/src/default/setting.ts @@ -2322,6 +2322,10 @@ When I am ___, I need ___ 'tools.klavis.disconnect': 'Disconnect', 'tools.klavis.disconnected': 'Disconnected', 'tools.klavis.error': 'Error', + 'tools.klavis.remove': 'Remove', + 'tools.klavis.removeConfirm.desc': + '{{name}} will be permanently removed from your connected services. This action cannot be undone.', + 'tools.klavis.removeConfirm.title': 'Remove {{name}}?', 'tools.klavis.groupName': 'Klavis Tools', 'tools.klavis.manage': 'Manage Klavis', 'tools.klavis.manageTitle': 'Manage Klavis Integration', diff --git a/src/features/Connectors/ConnectorDetail/index.tsx b/src/features/Connectors/ConnectorDetail/index.tsx index b1b2b21be9..9717e35389 100644 --- a/src/features/Connectors/ConnectorDetail/index.tsx +++ b/src/features/Connectors/ConnectorDetail/index.tsx @@ -1,5 +1,6 @@ +import { confirmModal } from '@lobehub/ui/base-ui'; import { Button } from 'antd'; -import { RefreshCwIcon } from 'lucide-react'; +import { RefreshCwIcon, Trash2 } from 'lucide-react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,9 +13,10 @@ import ToolPermissionGroup from './ToolPermissionGroup'; interface ConnectorDetailProps { connectorId: string; + onDelete?: () => void; } -const ConnectorDetail = memo(({ connectorId }) => { +const ConnectorDetail = memo(({ connectorId, onDelete }) => { const { t } = useTranslation('tool'); const connector = useToolStore(connectorSelectors.connectorById(connectorId)); @@ -28,6 +30,7 @@ const ConnectorDetail = memo(({ connectorId }) => { const syncPluginTools = useToolStore((s) => s.syncPluginTools); const resetConnectorPermissions = useToolStore((s) => s.resetConnectorPermissions); const disconnectConnector = useToolStore((s) => s.disconnectConnector); + const deleteConnector = useToolStore((s) => s.deleteConnector); const updateToolPermission = useToolStore((s) => s.updateToolPermission); const isMcpConnector = connector?.sourceType === ConnectorSourceType.custom; @@ -89,9 +92,28 @@ const ConnectorDetail = memo(({ connectorId }) => { {syncLabel} {isMcpConnector && ( - + <> + + + )} diff --git a/src/routes/(main)/settings/skill/features/KlavisSkillItem.tsx b/src/routes/(main)/settings/skill/features/KlavisSkillItem.tsx index 49bac9d543..c5897147c2 100644 --- a/src/routes/(main)/settings/skill/features/KlavisSkillItem.tsx +++ b/src/routes/(main)/settings/skill/features/KlavisSkillItem.tsx @@ -24,13 +24,14 @@ const POLL_TIMEOUT_MS = 15_000; interface KlavisSkillItemProps { isSelected?: boolean; + onDelete?: () => void; onSelect?: () => void; server?: KlavisServer; serverType: KlavisServerType; } const KlavisSkillItem = memo( - ({ serverType, server, isSelected, onSelect }) => { + ({ serverType, server, isSelected, onSelect, onDelete }) => { const { t } = useTranslation('setting'); const { allowed: canCreate, reason: createReason } = usePermission('create_content'); const { allowed: canEdit, reason: editReason } = usePermission('edit_own_content'); diff --git a/src/routes/(main)/settings/skill/features/LobehubSkillItem.tsx b/src/routes/(main)/settings/skill/features/LobehubSkillItem.tsx index a88e1fb012..d80c98daea 100644 --- a/src/routes/(main)/settings/skill/features/LobehubSkillItem.tsx +++ b/src/routes/(main)/settings/skill/features/LobehubSkillItem.tsx @@ -22,13 +22,14 @@ const POLL_TIMEOUT_MS = 15_000; interface LobehubSkillItemProps { isSelected?: boolean; + onDelete?: () => void; onSelect?: () => void; provider: LobehubSkillProviderType; server?: LobehubSkillServer; } const LobehubSkillItem = memo( - ({ provider, server, isSelected, onSelect }) => { + ({ provider, server, isSelected, onSelect, onDelete }) => { const { t } = useTranslation('setting'); const { allowed: canCreate, reason: createReason } = usePermission('create_content'); const { allowed: canEdit, reason: editReason } = usePermission('edit_own_content'); @@ -175,14 +176,17 @@ const LobehubSkillItem = memo( const handleDisconnect = () => { if (!canEdit) return; - if (!server) return; confirmModal({ cancelText: t('cancel', { ns: 'common' }), content: t('tools.lobehubSkill.disconnectConfirm.desc', { name: provider.label }), okButtonProps: { danger: true }, okText: t('tools.lobehubSkill.disconnect'), onOk: async () => { - await revokeConnect(server.identifier); + if (server) { + await revokeConnect(server.identifier); + } else if (isSelected) { + onDelete?.(); + } }, title: t('tools.lobehubSkill.disconnectConfirm.title', { name: provider.label }), }); diff --git a/src/routes/(main)/settings/skill/features/SkillDetail/index.tsx b/src/routes/(main)/settings/skill/features/SkillDetail/index.tsx index 180f0eaf37..1a75b117bd 100644 --- a/src/routes/(main)/settings/skill/features/SkillDetail/index.tsx +++ b/src/routes/(main)/settings/skill/features/SkillDetail/index.tsx @@ -1,13 +1,18 @@ 'use client'; import { Avatar, Markdown, Skeleton } from '@lobehub/ui'; +import { confirmModal } from '@lobehub/ui/base-ui'; +import { Button } from 'antd'; import { createStaticStyles } from 'antd-style'; import isEqual from 'fast-deep-equal'; +import { Plus, Trash2 } from 'lucide-react'; import { lazy, memo, Suspense, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { ConnectorDetail } from '@/features/Connectors'; +import { usePermission } from '@/hooks/usePermission'; import { useToolStore } from '@/store/tool'; -import { lobehubSkillStoreSelectors } from '@/store/tool/selectors'; +import { builtinToolSelectors, lobehubSkillStoreSelectors } from '@/store/tool/selectors'; import { connectorSelectors } from '@/store/tool/slices/connector'; const AgentSkillDetail = lazy(() => import('@/features/AgentSkillDetail')); @@ -29,8 +34,9 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ `, header: css` display: flex; - gap: 12px; + justify-content: space-between; align-items: flex-start; + gap: 12px; padding-block: 20px 16px; padding-inline: 24px; @@ -50,6 +56,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ interface SkillDetailProps { identifier: string; + onDelete?: () => void; type: ToolDetailType; } @@ -60,14 +67,21 @@ interface SkillDetailProps { * - 'builtin-skill': renders BuiltinSkill description panel (Artifacts, Task, etc.) * - 'builtin'/'plugin'/'mcp-connector': syncs connector entry, renders permission editor */ -const SkillDetail = memo(({ identifier, type }) => { +const SkillDetail = memo(({ identifier, type, onDelete }) => { + const { t } = useTranslation('plugin'); const [syncing, setSyncing] = useState(false); const [noManifest, setNoManifest] = useState(false); + const { allowed: canCreate } = usePermission('create_content'); + const { allowed: canEdit } = usePermission('edit_own_content'); + const syncBuiltinTool = useToolStore((s) => s.syncBuiltinTool); const syncPluginTools = useToolStore((s) => s.syncPluginTools); const syncToolsFromClient = useToolStore((s) => s.syncToolsFromClient); const fetchConnectors = useToolStore((s) => s.fetchConnectors); + const installBuiltinTool = useToolStore((s) => s.installBuiltinTool); + const uninstallBuiltinTool = useToolStore((s) => s.uninstallBuiltinTool); + const deleteAgentSkill = useToolStore((s) => s.deleteAgentSkill); const connector = useToolStore(connectorSelectors.connectorByIdentifier(identifier)); // For lobehub-connector: get the server's tool list from the store @@ -78,6 +92,7 @@ const SkillDetail = memo(({ identifier, type }) => { (s) => s.builtinSkills?.find((sk) => sk.identifier === identifier), isEqual, ); + const isBuiltinInstalled = useToolStore(builtinToolSelectors.isBuiltinToolInstalled(identifier)); const isConnectorType = type === 'builtin' || @@ -127,20 +142,63 @@ const SkillDetail = memo(({ identifier, type }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [identifier, type, isConnectorType]); + const handleUninstallBuiltin = () => { + confirmModal({ + okButtonProps: { danger: true }, + onOk: async () => { + await uninstallBuiltinTool(identifier); + }, + title: t('store.actions.confirmUninstall'), + }); + }; + + const handleDeleteAgentSkill = () => { + confirmModal({ + okButtonProps: { danger: true }, + onOk: async () => { + await deleteAgentSkill(identifier); + onDelete?.(); + }, + title: t('store.actions.confirmUninstall'), + }); + }; + // ── Render by type ────────────────────────────────────────────────────────── if (type === 'agent-skill') { return ( -
- - -
- } +
+
- - + +
+
+ + +
+ } + > + + +
); } @@ -149,11 +207,29 @@ const SkillDetail = memo(({ identifier, type }) => { return (
- {builtinSkill?.avatar && } -
-
{builtinSkill?.name || identifier}
- {builtinSkill?.description && ( -
{builtinSkill.description}
+
+ {builtinSkill?.avatar && } +
+
{builtinSkill?.name || identifier}
+ {builtinSkill?.description && ( +
{builtinSkill.description}
+ )} +
+
+
+ {isBuiltinInstalled ? ( + + ) : ( + )}
@@ -184,7 +260,7 @@ const SkillDetail = memo(({ identifier, type }) => { ); } - return ; + return ; }); SkillDetail.displayName = 'SkillDetail'; diff --git a/src/routes/(main)/settings/skill/features/SkillList.tsx b/src/routes/(main)/settings/skill/features/SkillList.tsx index 86f1ea4733..0137700b41 100644 --- a/src/routes/(main)/settings/skill/features/SkillList.tsx +++ b/src/routes/(main)/settings/skill/features/SkillList.tsx @@ -81,13 +81,14 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ export type SkillViewMode = 'connector' | 'skill'; interface SkillListProps { + onDeleteSelected?: () => void; onSelect?: (identifier: string, type: ToolDetailType) => void; selectedIdentifier?: string; viewMode?: SkillViewMode; } const SkillList = memo( - ({ onSelect, selectedIdentifier, viewMode = 'connector' }) => { + ({ onSelect, onDeleteSelected, selectedIdentifier, viewMode = 'connector' }) => { const { t } = useTranslation('setting'); const [collapsed, setCollapsed] = useState>(new Set()); @@ -507,6 +508,7 @@ const SkillList = memo( key={item.provider.id} provider={item.provider} server={getLobehubSkillServerByProvider(item.provider.id)} + onDelete={onDeleteSelected} onSelect={ onSelect ? () => onSelect(item.provider.id, 'lobehub-connector') : undefined } @@ -519,6 +521,7 @@ const SkillList = memo( key={item.serverType.identifier} server={getKlavisServerByIdentifier(item.serverType.identifier)} serverType={item.serverType} + onDelete={onDeleteSelected} onSelect={ onSelect ? () => onSelect(item.serverType.identifier, 'plugin') : undefined } diff --git a/src/routes/(main)/settings/skill/index.tsx b/src/routes/(main)/settings/skill/index.tsx index a2aa34f2a7..37841f0bc7 100644 --- a/src/routes/(main)/settings/skill/index.tsx +++ b/src/routes/(main)/settings/skill/index.tsx @@ -225,6 +225,7 @@ const Page = memo(() => { setSelected(null)} onSelect={handleSelect} />
@@ -233,7 +234,11 @@ const Page = memo(() => { {/* Right: tool detail + permissions */} {selected && (
- + setSelected(null)} + />
)}