feat: hide add library skill in agent ui (#41250)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
wangxiaolei 2026-08-25 10:26:06 +00:00 committed by GitHub
parent 960018253b
commit a6fe278662
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 120 additions and 54 deletions

View File

@ -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

View File

@ -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,
)

View File

@ -18209,7 +18209,7 @@ Flask blueprint initialization.
| docs_processing | string, <br>**Default:** standard | | Yes |
| documents_upload_quota | [LimitationModel](#limitationmodel) | | Yes |
| education | [EducationModel](#educationmodel) | | Yes |
| enable_skill | boolean | | Yes |
| enable_skill | boolean, <br>**Default:** true | | Yes |
| human_input_email_delivery_enabled | boolean | | Yes |
| is_allow_transfer_workspace | boolean, <br>**Default:** true | | Yes |
| knowledge_pipeline | [KnowledgePipeline](#knowledgepipeline) | | Yes |

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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 }),

View File

@ -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: [] }),

View File

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

View File

@ -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}

View File

@ -53,6 +53,7 @@ type AgentPromptSlashMenuProps = {
onAddFile?: AgentOrchestrateAddAction
onAddKnowledge?: AgentOrchestrateAddAction
onAddSkill?: AgentOrchestrateAddAction
canAddWorkspaceSkill?: boolean
knowledgeRetrievals: AgentKnowledgeRetrievalItem[]
onBack: () => void
onOpenCategory: (view: Exclude<SlashMenuView, 'main'>) => 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' ? (
<div className="flex flex-col border-t border-divider-subtle p-1">
<AgentPromptSkillAddButton
icon="i-custom-vender-agent-v2-building-blocks"
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
onClick={() => handleAddFromFooter('library')}
/>
{canAddWorkspaceSkill && (
<AgentPromptSkillAddButton
icon="i-custom-vender-agent-v2-building-blocks"
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
onClick={() => handleAddFromFooter('library')}
/>
)}
<AgentPromptSkillAddButton
icon="i-ri-upload-cloud-2-line"
label={t(($) => $['agentDetail.configure.skills.addMenu.upload.label'])}

View File

@ -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:

View File

@ -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<AgentOrchestrateAddActionOptions['onAdded']>(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"
>
<span aria-hidden className="i-ri-add-line size-3.5" />
<span>{tCommon(($) => $['operation.add'])}</span>
@ -732,14 +741,16 @@ export function AgentSkills() {
>
{addMenuView === 'menu' ? (
<>
<AgentSkillAddMenuItem
iconClassName="i-custom-vender-agent-v2-building-blocks"
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
description={t(
($) => $['agentDetail.configure.skills.addMenu.workspace.description'],
)}
onClick={handleOpenWorkspaceSelector}
/>
{enableSkill && (
<AgentSkillAddMenuItem
iconClassName="i-custom-vender-agent-v2-building-blocks"
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
description={t(
($) => $['agentDetail.configure.skills.addMenu.workspace.description'],
)}
onClick={handleOpenWorkspaceSelector}
/>
)}
<AgentSkillAddMenuItem
badge={t(($) => $['agentDetail.configure.skills.addMenu.upload.badge'])}
iconClassName="i-ri-upload-cloud-2-line"
@ -751,11 +762,15 @@ export function AgentSkills() {
/>
</>
) : (
<WorkspaceSkillSelector
boundSkillIds={boundSkillIds}
isBindingPending={!hasLoadedAgentSkillBindings || isReplacingAgentSkillBindings}
onSelect={handleSelectWorkspaceSkill}
/>
enableSkill && (
<WorkspaceSkillSelector
boundSkillIds={boundSkillIds}
isBindingPending={
!hasLoadedAgentSkillBindings || isReplacingAgentSkillBindings
}
onSelect={handleSelectWorkspaceSkill}
/>
)
)}
</PopoverContent>
</Popover>

View File

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