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(
() => {