From a6fe2786629360a4dabfa53bfaa6b7c39fba4d2a Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Tue, 25 Aug 2026 10:26:06 +0000 Subject: [PATCH] feat: hide add library skill in agent ui (#41250) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/.env.example | 2 +- api/configs/feature/__init__.py | 2 +- api/openapi/markdown/console-openapi.md | 2 +- api/services/entities/feature_entities.py | 2 +- .../services/test_feature_service_skill.py | 2 +- docker/envs/core-services/shared.env.example | 2 +- .../generated/api/console/features/zod.gen.ts | 2 +- .../__tests__/agent-prompt-editor.spec.tsx | 5 ++ .../components/orchestrate/config-context.ts | 9 ++- .../orchestrate/prompt-editor/index.tsx | 3 + .../orchestrate/prompt-editor/slash.tsx | 14 ++-- .../skills/__tests__/index.spec.tsx | 44 ++++++++++- .../components/orchestrate/skills/index.tsx | 77 +++++++++++-------- .../skills/__tests__/detail-page.spec.tsx | 8 +- 14 files changed, 120 insertions(+), 54 deletions(-) diff --git a/api/.env.example b/api/.env.example index b9a6fbe1cbf..389fc5afa8d 100644 --- a/api/.env.example +++ b/api/.env.example @@ -852,4 +852,4 @@ DIFY_ENV_NACOS_REQUEST_TIMEOUT=10.0 DIFY_ENV_NACOS_CONNECT_TIMEOUT=3.0 # skill entry -ENABLE_SKILL=false +ENABLE_SKILL=true diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index e498e1428a9..8098f3cbf2c 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1294,7 +1294,7 @@ class DataSetConfig(BaseSettings): class SkillConfig(BaseSettings): ENABLE_SKILL: bool = Field( description="Enable or disable Skill feature entry points", - default=False, + default=True, ) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 034a0476682..bf522b09443 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -18209,7 +18209,7 @@ Flask blueprint initialization. | docs_processing | string,
**Default:** standard | | Yes | | documents_upload_quota | [LimitationModel](#limitationmodel) | | Yes | | education | [EducationModel](#educationmodel) | | Yes | -| enable_skill | boolean | | Yes | +| enable_skill | boolean,
**Default:** true | | Yes | | human_input_email_delivery_enabled | boolean | | Yes | | is_allow_transfer_workspace | boolean,
**Default:** true | | Yes | | knowledge_pipeline | [KnowledgePipeline](#knowledgepipeline) | | Yes | diff --git a/api/services/entities/feature_entities.py b/api/services/entities/feature_entities.py index e8b959e9eb3..f35c558d745 100644 --- a/api/services/entities/feature_entities.py +++ b/api/services/entities/feature_entities.py @@ -151,7 +151,7 @@ class PluginInstallationPermissionModel(FeatureResponseModel): class FeatureModel(FeatureResponseModel): billing: BillingModel = BillingModel() education: EducationModel = EducationModel() - enable_skill: bool = False + enable_skill: bool = True members: LimitationModel = LimitationModel(size=0, limit=1) apps: LimitationModel = LimitationModel(size=0, limit=10) vector_space: LimitationModel | None = LimitationModel(size=0, limit=5) diff --git a/api/tests/unit_tests/services/test_feature_service_skill.py b/api/tests/unit_tests/services/test_feature_service_skill.py index c0d7530c03b..60c15d0bd24 100644 --- a/api/tests/unit_tests/services/test_feature_service_skill.py +++ b/api/tests/unit_tests/services/test_feature_service_skill.py @@ -6,7 +6,7 @@ from services.feature_service import FeatureService def test_skill_feature_is_disabled_by_default() -> None: - assert FeatureModel().enable_skill is False + assert FeatureModel().enable_skill is True def test_skill_feature_follows_env_config(config_overrides: Callable[..., None]) -> None: diff --git a/docker/envs/core-services/shared.env.example b/docker/envs/core-services/shared.env.example index 2f9dc30aae7..784682d426b 100644 --- a/docker/envs/core-services/shared.env.example +++ b/docker/envs/core-services/shared.env.example @@ -32,7 +32,7 @@ ENABLE_TRIAL_APP=false ENABLE_EXPLORE_BANNER=false ENABLE_LEARN_APP=true ENABLE_STEP_BY_STEP_TOUR=false -ENABLE_SKILL=false +ENABLE_SKILL=true RBAC_ENABLED=false ENABLE_LICENSE_EXPIRY_NOTICE=true CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1 diff --git a/packages/contracts/generated/api/console/features/zod.gen.ts b/packages/contracts/generated/api/console/features/zod.gen.ts index aeaea985907..8978ebf9cae 100644 --- a/packages/contracts/generated/api/console/features/zod.gen.ts +++ b/packages/contracts/generated/api/console/features/zod.gen.ts @@ -103,7 +103,7 @@ export const zFeatureModel = z.object({ docs_processing: z.string().default('standard'), documents_upload_quota: zLimitationModel.default({ limit: 50, size: 0 }), education: zEducationModel.default({ activated: false, enabled: false }), - enable_skill: z.boolean().default(false), + enable_skill: z.boolean().default(true), human_input_email_delivery_enabled: z.boolean().default(false), is_allow_transfer_workspace: z.boolean().default(true), knowledge_pipeline: zKnowledgePipeline.default({ publish_enabled: false }), diff --git a/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx index 968d4dc5672..48fde00309c 100644 --- a/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx @@ -199,6 +199,11 @@ vi.mock('@/context/workspace-state', async () => { })) }) +vi.mock('@/context/provider-context', () => ({ + useProviderContextSelector: (selector: (state: { enableSkill: boolean }) => unknown) => + selector({ enableSkill: true }), +})) + vi.mock('@/service/use-tools', () => ({ useAllBuiltInTools: () => ({ data: mockBuiltInTools }), useAllCustomTools: () => ({ data: [] }), diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/config-context.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/config-context.ts index 91ff420c215..8b2a3b18e14 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/config-context.ts +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/config-context.ts @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { createContext, use } from 'react' +import { useProviderContextSelector } from '@/context/provider-context' import { agentComposerFilesAtom } from '@/features/agent-v2/agent-composer/store-modules/files' import { agentComposerSkillsAtom } from '@/features/agent-v2/agent-composer/store-modules/skills' import { consoleQuery } from '@/service/client' @@ -40,16 +41,18 @@ export const useAgentConfigSkills = () => { export const useAgentWorkspaceSkillBindings = () => { const { agentId } = useAgentConfigApiContext() + const enableSkill = useProviderContextSelector((state) => state.enableSkill) - return useQuery( - consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryOptions({ + return useQuery({ + ...consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryOptions({ input: { params: { agent_id: agentId, }, }, }), - ) + enabled: enableSkill, + }) } export const useAgentConfigFiles = () => { diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx index 6fec81ed7b1..c77e9d85e31 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx @@ -38,6 +38,7 @@ import { Infotip } from '@/app/components/base/infotip' import PromptEditor from '@/app/components/base/prompt-editor' import BlockIcon from '@/app/components/workflow/block-icon' import { BlockEnum } from '@/app/components/workflow/types' +import { useProviderContextSelector } from '@/context/provider-context' import { agentComposerKnowledgeRetrievalsAtom } from '@/features/agent-v2/agent-composer/store-modules/knowledge' import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt' import { @@ -421,6 +422,7 @@ function AgentPromptSelectionBridge({ export function AgentPromptEditor() { const { t } = useTranslation('agentV2') const readOnly = useAgentOrchestrateReadOnly() + const enableSkill = useProviderContextSelector((state) => state.enableSkill) const [value, setValue] = useAtom(agentComposerPromptAtom) const { skills: embeddedSkills } = useAgentConfigSkills() const workspaceSkillBindingsQuery = useAgentWorkspaceSkillBindings() @@ -1058,6 +1060,7 @@ export function AgentPromptEditor() { onAddFile={addActions.files} onAddKnowledge={addActions.knowledge} onAddSkill={addActions.skills} + canAddWorkspaceSkill={enableSkill} knowledgeRetrievals={retrievals} onBack={returnToSlashMenuMain} onOpenCategory={handleOpenSlashMenuCategory} diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx index 0c2f274c3cb..38c0c3341a5 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx @@ -53,6 +53,7 @@ type AgentPromptSlashMenuProps = { onAddFile?: AgentOrchestrateAddAction onAddKnowledge?: AgentOrchestrateAddAction onAddSkill?: AgentOrchestrateAddAction + canAddWorkspaceSkill?: boolean knowledgeRetrievals: AgentKnowledgeRetrievalItem[] onBack: () => void onOpenCategory: (view: Exclude) => void @@ -95,6 +96,7 @@ export function AgentPromptSlashMenu({ onAddFile, onAddKnowledge, onAddSkill, + canAddWorkspaceSkill = true, knowledgeRetrievals, onBack, onOpenCategory, @@ -215,11 +217,13 @@ export function AgentPromptSlashMenu({ /> ) : view === 'skills' ? (
- $['agentDetail.configure.skills.addMenu.workspace.label'])} - onClick={() => handleAddFromFooter('library')} - /> + {canAddWorkspaceSkill && ( + $['agentDetail.configure.skills.addMenu.workspace.label'])} + onClick={() => handleAddFromFooter('library')} + /> + )} $['agentDetail.configure.skills.addMenu.upload.label'])} diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx index 6078fb406e5..34c72fa00a9 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx @@ -81,6 +81,9 @@ const mocks = vi.hoisted(() => ({ fileUploadConfig: { skill_file_size_limit: 64, }, + providerContext: { + enableSkill: true, + }, })) vi.mock('@langgenius/dify-ui/toast', () => ({ @@ -112,6 +115,11 @@ vi.mock('@/context/permission-state', async () => { })) }) +vi.mock('@/context/provider-context', () => ({ + useProviderContextSelector: (selector: (state: { enableSkill: boolean }) => unknown) => + selector(mocks.providerContext), +})) + vi.mock('@/service/client', () => ({ consoleQuery: { tags: { @@ -342,6 +350,7 @@ function renderAgentSkills({ describe('AgentSkills', () => { beforeEach(() => { vi.clearAllMocks() + mocks.providerContext.enableSkill = true mocks.fileUploadConfig.skill_file_size_limit = 64 vi.stubGlobal('fetch', mocks.fetch) document.cookie = 'csrf_token=csrf-token; path=/' @@ -732,9 +741,14 @@ describe('AgentSkills', () => { }) renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState }) - await user.click( - screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }), - ) + const addButton = screen.getByRole('button', { + name: /agentV2\.agentDetail\.configure\.skills\.add/i, + }) + expect(addButton).not.toHaveAttribute('data-popup-open') + + await user.click(addButton) + expect(addButton).toHaveAttribute('data-popup-open', '') + const workspaceMenuItem = screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i, }) @@ -770,6 +784,30 @@ describe('AgentSkills', () => { expect(toast.success).not.toHaveBeenCalled() }) + it('should hide workspace skill selection when skill is disabled', async () => { + const user = userEvent.setup() + mocks.providerContext.enableSkill = false + + renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState }) + + await user.click( + screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }), + ) + + expect( + screen.queryByRole('button', { + name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i, + }), + ).not.toBeInTheDocument() + expect( + screen.getByRole('button', { + name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.upload\.label/i, + }), + ).toBeInTheDocument() + expect(mocks.agentSkillBindingsQueryOptions).toHaveBeenCalledWith(expect.anything()) + expect(mocks.workspaceSkillsInfiniteOptions).not.toHaveBeenCalled() + }) + it('should not replace existing workspace skill bindings before they finish loading', async () => { const user = userEvent.setup() let resolveBindings: diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx index ff34ccb1d65..0e7222f3a91 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx @@ -31,6 +31,7 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' import { SkeletonRectangle } from '@/app/components/base/skeleton' +import { useProviderContextSelector } from '@/context/provider-context' import { agentComposerSkillsAtom, removeAgentSkillAtom, @@ -466,6 +467,7 @@ export function AgentSkills() { const [isUploadOpen, setIsUploadOpen] = useState(false) const promptAddCallbackRef = useRef(undefined) const apiContext = useAgentConfigApiContext() + const enableSkill = useProviderContextSelector((state) => state.enableSkill) const skills = useAtomValue(agentComposerSkillsAtom) const upsertAgentSkill = useSetAtom(upsertAgentSkillAtom) const removeAgentSkill = useSetAtom(removeAgentSkillAtom) @@ -485,6 +487,7 @@ export function AgentSkills() { }) const agentSkillBindingsQuery = useQuery({ ...agentSkillBindingsQueryOptions, + enabled: enableSkill, }) const hasLoadedAgentSkillBindings = agentSkillBindingsQuery.data !== undefined const { isPending: isReplacingAgentSkillBindings, mutate: replaceAgentSkillBindings } = @@ -568,22 +571,25 @@ export function AgentSkills() { ], ) - const handlePromptAdd = useCallback((options?: AgentOrchestrateAddActionOptions) => { - promptAddCallbackRef.current = options?.onAdded - if (options?.skillSource === 'library') { - setAddMenuView('workspace-selector') + const handlePromptAdd = useCallback( + (options?: AgentOrchestrateAddActionOptions) => { + promptAddCallbackRef.current = options?.onAdded + if (options?.skillSource === 'library' && enableSkill) { + setAddMenuView('workspace-selector') + setAddMenuOpen(true) + return + } + + if (options?.skillSource === 'upload') { + setIsUploadOpen(true) + return + } + + setAddMenuView('menu') setAddMenuOpen(true) - return - } - - if (options?.skillSource === 'upload') { - setIsUploadOpen(true) - return - } - - setAddMenuView('menu') - setAddMenuOpen(true) - }, []) + }, + [enableSkill], + ) useRegisterAgentOrchestrateAddAction('skills', handlePromptAdd) const handleAddMenuOpenChange = useCallback((open: boolean) => { @@ -595,8 +601,10 @@ export function AgentSkills() { }, []) const handleOpenWorkspaceSelector = useCallback(() => { + if (!enableSkill) return + setAddMenuView('workspace-selector') - }, []) + }, [enableSkill]) const handleOpenUploadFromMenu = useCallback(() => { setAddMenuOpen(false) @@ -615,6 +623,7 @@ export function AgentSkills() { const handleSelectWorkspaceSkill = useCallback( (skill: SkillResponse) => { if ( + !enableSkill || !hasLoadedAgentSkillBindings || !skill.latest_published_version_id || boundSkillIds.includes(skill.id) @@ -636,7 +645,7 @@ export function AgentSkills() { setAddMenuView('menu') }) }, - [boundSkillIds, hasLoadedAgentSkillBindings, replaceWorkspaceSkillBindings, t], + [boundSkillIds, enableSkill, hasLoadedAgentSkillBindings, replaceWorkspaceSkillBindings, t], ) const handleUploadOpenChange = useCallback((open: boolean) => { @@ -714,7 +723,7 @@ export function AgentSkills() { aria-label={t(($) => $['agentDetail.configure.skills.add'])} variant="ghost" size="small" - className="shrink-0 gap-1 px-2" + className="shrink-0 gap-1 px-2 data-popup-open:bg-state-base-hover" > {tCommon(($) => $['operation.add'])} @@ -732,14 +741,16 @@ export function AgentSkills() { > {addMenuView === 'menu' ? ( <> - $['agentDetail.configure.skills.addMenu.workspace.label'])} - description={t( - ($) => $['agentDetail.configure.skills.addMenu.workspace.description'], - )} - onClick={handleOpenWorkspaceSelector} - /> + {enableSkill && ( + $['agentDetail.configure.skills.addMenu.workspace.label'])} + description={t( + ($) => $['agentDetail.configure.skills.addMenu.workspace.description'], + )} + onClick={handleOpenWorkspaceSelector} + /> + )} $['agentDetail.configure.skills.addMenu.upload.badge'])} iconClassName="i-ri-upload-cloud-2-line" @@ -751,11 +762,15 @@ export function AgentSkills() { /> ) : ( - + enableSkill && ( + + ) )} diff --git a/web/features/skills/__tests__/detail-page.spec.tsx b/web/features/skills/__tests__/detail-page.spec.tsx index 7f3a4bed022..1ba179b29b0 100644 --- a/web/features/skills/__tests__/detail-page.spec.tsx +++ b/web/features/skills/__tests__/detail-page.spec.tsx @@ -2366,7 +2366,6 @@ describe('SkillDetailPage', () => { }) it('updates and removes existing custom metadata from the manifest editor', async () => { - const user = userEvent.setup() const content = '---\nname: github-actions-failure-debugging\ndescription: Guide for debugging failing GitHub Actions workflows.\nmetadata:\n display-name: Untitled skill\n owner: support\n---\n# GitHub Actions Failure Debugging\n' mocks.skillDetail = createSkillDetail({ @@ -2382,9 +2381,8 @@ describe('SkillDetailPage', () => { renderSkillDetailPage() const ownerValue = await screen.findByRole('textbox', { name: 'owner value' }) - await user.clear(ownerValue) - await user.type(ownerValue, 'success') - await user.tab() + fireEvent.change(ownerValue, { target: { value: 'success' } }) + fireEvent.blur(ownerValue) await waitFor( () => { @@ -2401,7 +2399,7 @@ describe('SkillDetailPage', () => { { timeout: 2500 }, ) - await user.click(screen.getByRole('button', { name: 'Remove owner' })) + fireEvent.click(screen.getByRole('button', { name: 'Remove owner' })) await waitFor( () => {