mirror of
https://github.com/langgenius/dify.git
synced 2026-08-29 03:45:08 +08:00
feat: add skill enable feature switch (#41246)
This commit is contained in:
@@ -850,3 +850,6 @@ CONVERSATION_CLEANUP_BATCH_SIZE=100
|
||||
DIFY_ENV_NACOS_REQUEST_TIMEOUT=10.0
|
||||
# Connect timeout for Nacos requests (default: 3.0)
|
||||
DIFY_ENV_NACOS_CONNECT_TIMEOUT=3.0
|
||||
|
||||
# skill entry
|
||||
ENABLE_SKILL=false
|
||||
|
||||
@@ -1291,6 +1291,13 @@ class DataSetConfig(BaseSettings):
|
||||
)
|
||||
|
||||
|
||||
class SkillConfig(BaseSettings):
|
||||
ENABLE_SKILL: bool = Field(
|
||||
description="Enable or disable Skill feature entry points",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceConfig(BaseSettings):
|
||||
"""
|
||||
Configuration for workspace management
|
||||
@@ -1685,6 +1692,7 @@ class FeatureConfig(
|
||||
RepositoryConfig,
|
||||
SandboxExpiredRecordsCleanConfig,
|
||||
SecurityConfig,
|
||||
SkillConfig,
|
||||
TenantIsolatedTaskQueueConfig,
|
||||
ToolConfig,
|
||||
UpdateConfig,
|
||||
|
||||
@@ -18209,6 +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 |
|
||||
| human_input_email_delivery_enabled | boolean | | Yes |
|
||||
| is_allow_transfer_workspace | boolean, <br>**Default:** true | | Yes |
|
||||
| knowledge_pipeline | [KnowledgePipeline](#knowledgepipeline) | | Yes |
|
||||
|
||||
@@ -151,6 +151,7 @@ class PluginInstallationPermissionModel(FeatureResponseModel):
|
||||
class FeatureModel(FeatureResponseModel):
|
||||
billing: BillingModel = BillingModel()
|
||||
education: EducationModel = EducationModel()
|
||||
enable_skill: bool = False
|
||||
members: LimitationModel = LimitationModel(size=0, limit=1)
|
||||
apps: LimitationModel = LimitationModel(size=0, limit=10)
|
||||
vector_space: LimitationModel | None = LimitationModel(size=0, limit=5)
|
||||
|
||||
@@ -214,6 +214,7 @@ class FeatureService:
|
||||
features.model_load_balancing_enabled = dify_config.MODEL_LB_ENABLED
|
||||
features.dataset_operator_enabled = dify_config.DATASET_OPERATOR_ENABLED
|
||||
features.education.enabled = dify_config.EDUCATION_ENABLED
|
||||
features.enable_skill = dify_config.ENABLE_SKILL
|
||||
|
||||
@classmethod
|
||||
def _fulfill_params_from_workspace_info(cls, features: feature_entities.FeatureModel, tenant_id: str):
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from enums import DeploymentEdition
|
||||
from services.entities.feature_entities import FeatureModel
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
def test_skill_feature_is_disabled_by_default() -> None:
|
||||
assert FeatureModel().enable_skill is False
|
||||
|
||||
|
||||
def test_skill_feature_follows_env_config(config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY, ENABLE_SKILL=True)
|
||||
|
||||
features = FeatureService.get_features("")
|
||||
|
||||
assert features.enable_skill is True
|
||||
@@ -32,6 +32,7 @@ ENABLE_TRIAL_APP=false
|
||||
ENABLE_EXPLORE_BANNER=false
|
||||
ENABLE_LEARN_APP=true
|
||||
ENABLE_STEP_BY_STEP_TOUR=false
|
||||
ENABLE_SKILL=false
|
||||
RBAC_ENABLED=false
|
||||
ENABLE_LICENSE_EXPIRY_NOTICE=true
|
||||
CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1
|
||||
|
||||
@@ -14,6 +14,7 @@ export type FeatureModel = {
|
||||
docs_processing: string
|
||||
documents_upload_quota: LimitationModel
|
||||
education: EducationModel
|
||||
enable_skill: boolean
|
||||
human_input_email_delivery_enabled: boolean
|
||||
is_allow_transfer_workspace: boolean
|
||||
knowledge_pipeline: KnowledgePipeline
|
||||
|
||||
@@ -103,6 +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),
|
||||
human_input_email_delivery_enabled: z.boolean().default(false),
|
||||
is_allow_transfer_workspace: z.boolean().default(true),
|
||||
knowledge_pipeline: zKnowledgePipeline.default({ publish_enabled: false }),
|
||||
|
||||
@@ -19,6 +19,7 @@ export const baseProviderContextValue: ProviderContextState = {
|
||||
isFetchedPlan: false,
|
||||
isFetchedPlanInfo: false,
|
||||
enableBilling: false,
|
||||
enableSkill: false,
|
||||
onPlanInfoChanged: noop,
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ const defaultProviderContext = {
|
||||
isFetchedPlan: false,
|
||||
isFetchedPlanInfo: false,
|
||||
enableBilling: false,
|
||||
enableSkill: false,
|
||||
onPlanInfoChanged: noop,
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
|
||||
@@ -78,6 +78,7 @@ describe('billing utils', () => {
|
||||
activated: false,
|
||||
enabled: false,
|
||||
},
|
||||
enable_skill: false,
|
||||
human_input_email_delivery_enabled: false,
|
||||
is_allow_transfer_workspace: false,
|
||||
knowledge_pipeline: {
|
||||
|
||||
@@ -168,6 +168,11 @@ type MainNavConsoleState = ConsoleStateFixture & {
|
||||
const mockConsoleState = vi.hoisted(() => ({
|
||||
current: undefined as MainNavConsoleState | undefined,
|
||||
}))
|
||||
const mockProviderContextState = vi.hoisted(() => ({
|
||||
current: {
|
||||
enableSkill: true,
|
||||
} as Partial<ProviderContextState>,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/agent-v2/feature-flag', () => ({
|
||||
isAgentV2Enabled: () => mockIsAgentV2Enabled(),
|
||||
@@ -187,6 +192,9 @@ vi.mock('@/context/permission-state', async () => {
|
||||
})
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
useProviderContextSelector: vi.fn((selector: (state: Partial<ProviderContextState>) => unknown) =>
|
||||
selector(mockProviderContextState.current),
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
@@ -638,6 +646,9 @@ describe('MainNav', () => {
|
||||
refresh: vi.fn(),
|
||||
})
|
||||
mockConsoleState.current = consoleState
|
||||
mockProviderContextState.current = {
|
||||
enableSkill: true,
|
||||
}
|
||||
;(useProviderContext as Mock).mockReturnValue({
|
||||
enableBilling: true,
|
||||
enableEducationPlan: false,
|
||||
@@ -759,6 +770,16 @@ describe('MainNav', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides the skills entry when skill is disabled', () => {
|
||||
mockProviderContextState.current = {
|
||||
enableSkill: false,
|
||||
}
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.queryByRole('link', { name: /common.mainNav.skills/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('orders the Step-by-step Tour before the account and help actions', async () => {
|
||||
localStorage.setItem(STEP_BY_STEP_TOUR_SHELL_MODE_STORAGE_KEY, 'collapsed')
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import Badge from '@/app/components/base/badge'
|
||||
import { DifyLogo } from '@/app/components/base/logo/dify-logo'
|
||||
import EnvNav from '@/app/components/header/env-nav'
|
||||
import StepByStepTourMount from '@/app/components/step-by-step-tour/mount'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { isCurrentWorkspaceDatasetOperatorAtom } from '@/context/workspace-state'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
|
||||
@@ -38,6 +39,7 @@ export function MainNav({ className }: MainNavProps) {
|
||||
})
|
||||
const agentV2Enabled = isAgentV2Enabled()
|
||||
const canManageAgents = useCanManageAgents()
|
||||
const enableSkill = useProviderContextSelector((state) => state.enableSkill)
|
||||
const showEnvTag = currentEnv === 'TESTING' || currentEnv === 'DEVELOPMENT'
|
||||
const helpMenuTriggerRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -49,6 +51,7 @@ export function MainNav({ className }: MainNavProps) {
|
||||
canManageAgents,
|
||||
isCurrentWorkspaceDatasetOperator,
|
||||
marketplaceEnabled: systemFeatures.enable_marketplace,
|
||||
skillEnabled: enableSkill,
|
||||
}),
|
||||
).map((route) => ({
|
||||
href: route.href,
|
||||
@@ -60,6 +63,7 @@ export function MainNav({ className }: MainNavProps) {
|
||||
[
|
||||
agentV2Enabled,
|
||||
canManageAgents,
|
||||
enableSkill,
|
||||
isCurrentWorkspaceDatasetOperator,
|
||||
systemFeatures.enable_marketplace,
|
||||
t,
|
||||
|
||||
@@ -20,6 +20,7 @@ export type MainNavRouteVisibilityOptions = {
|
||||
canManageAgents: boolean
|
||||
isCurrentWorkspaceDatasetOperator: boolean
|
||||
marketplaceEnabled: boolean
|
||||
skillEnabled: boolean
|
||||
}
|
||||
|
||||
export type DetailSidebarVisibilityOptions = Pick<
|
||||
@@ -29,8 +30,8 @@ export type DetailSidebarVisibilityOptions = Pick<
|
||||
|
||||
const VISIBLE_TO_ALL: MainNavRouteVisibility = () => true
|
||||
const CAN_MANAGE_AGENTS: MainNavRouteVisibility = (options) => options.canManageAgents
|
||||
const NOT_DATASET_OPERATOR: MainNavRouteVisibility = (options) =>
|
||||
!options.isCurrentWorkspaceDatasetOperator
|
||||
const SKILL_ENABLED_FOR_WORKSPACE: MainNavRouteVisibility = (options) =>
|
||||
options.skillEnabled && !options.isCurrentWorkspaceDatasetOperator
|
||||
|
||||
function isPathUnderRoute(pathname: string, route: string) {
|
||||
return pathname === route || pathname.startsWith(`${route}/`)
|
||||
@@ -84,7 +85,7 @@ export const MAIN_NAV_ROUTES = [
|
||||
active: (path: string) => isPathUnderRoute(path, '/skills'),
|
||||
icon: 'i-custom-vender-main-nav-skill',
|
||||
activeIcon: 'i-custom-vender-main-nav-skill-active',
|
||||
visibility: NOT_DATASET_OPERATOR,
|
||||
visibility: SKILL_ENABLED_FOR_WORKSPACE,
|
||||
},
|
||||
{
|
||||
key: 'integrations',
|
||||
|
||||
@@ -43,6 +43,7 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro
|
||||
const isFetchedPlan = featuresQuery.isSuccess && enableBilling
|
||||
const isFetchedPlanInfo = featuresQuery.isFetched
|
||||
const enableEducationPlan = features?.education.enabled ?? false
|
||||
const enableSkill = features?.enable_skill ?? false
|
||||
const enableReplaceWebAppLogo = features?.can_replace_logo ?? false
|
||||
const modelLoadBalancingEnabled = features?.model_load_balancing_enabled ?? false
|
||||
const webappCopyrightEnabled = features?.webapp_copyright_enabled ?? false
|
||||
@@ -97,6 +98,7 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro
|
||||
isFetchedPlan,
|
||||
isFetchedPlanInfo,
|
||||
enableBilling,
|
||||
enableSkill,
|
||||
onPlanInfoChanged: refreshFeatures,
|
||||
enableReplaceWebAppLogo,
|
||||
modelLoadBalancingEnabled,
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ProviderContextState = {
|
||||
isFetchedPlan: boolean
|
||||
isFetchedPlanInfo: boolean
|
||||
enableBilling: boolean
|
||||
enableSkill: boolean
|
||||
onPlanInfoChanged: () => void
|
||||
enableReplaceWebAppLogo: boolean
|
||||
modelLoadBalancingEnabled: boolean
|
||||
@@ -53,6 +54,7 @@ export const baseProviderContextValue: ProviderContextState = {
|
||||
isFetchedPlan: false,
|
||||
isFetchedPlanInfo: false,
|
||||
enableBilling: false,
|
||||
enableSkill: false,
|
||||
onPlanInfoChanged: noop,
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
|
||||
@@ -71,7 +71,7 @@ export function WebAppAccessCard({
|
||||
})
|
||||
const toggleSiteMutation = useMutation(
|
||||
consoleQuery.apps.byAppId.siteEnable.post.mutationOptions({
|
||||
onSuccess: (_updatedApp, variables) => {
|
||||
onSuccess: async (_updatedApp, variables) => {
|
||||
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
|
||||
agentDetailQueryKey,
|
||||
(agentDetail) =>
|
||||
@@ -82,6 +82,7 @@ export function WebAppAccessCard({
|
||||
}
|
||||
: agentDetail,
|
||||
)
|
||||
await queryClient.invalidateQueries({ queryKey: agentDetailQueryKey })
|
||||
toast.success(tCommon(($) => $['actionMsg.modifiedSuccessfully']))
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
Reference in New Issue
Block a user