From bcd573e560297cccc46b8551efd5b4fef8c5e9b8 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:11:58 +0800 Subject: [PATCH] fix(web): respect marketplace feature flag in model selector (#36883) --- .../__tests__/hooks.spec.ts | 30 +++++++++++++ .../model-provider-page/hooks.ts | 23 +++++++--- .../model-selector/__tests__/popup.spec.tsx | 34 +++++++++++++- .../model-selector/popup.tsx | 45 ++++++++++++------- 4 files changed, 107 insertions(+), 25 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/__tests__/hooks.spec.ts b/web/app/components/header/account-setting/model-provider-page/__tests__/hooks.spec.ts index 9b592e9fff..cbf0fc2c9d 100644 --- a/web/app/components/header/account-setting/model-provider-page/__tests__/hooks.spec.ts +++ b/web/app/components/header/account-setting/model-provider-page/__tests__/hooks.spec.ts @@ -1264,6 +1264,36 @@ describe('hooks', () => { expect(result.current.plugins).toEqual(searchPlugins) expect(result.current.plugins?.some(p => p.plugin_id === 'collection-only')).toBe(false) }) + + it('should skip marketplace queries when disabled', () => { + const queryPlugins = vi.fn() + const queryPluginsWithDebounced = vi.fn() + const cancelQueryPluginsWithDebounced = vi.fn() + const resetPlugins = vi.fn() + + ; (useMarketplacePluginsByCollectionId as Mock).mockReturnValue({ + plugins: [{ plugin_id: 'collection-only', type: 'plugin' }], + isLoading: true, + }) + ; (useMarketplacePlugins as Mock).mockReturnValue({ + plugins: [{ plugin_id: 'search-result', type: 'plugin' }], + queryPlugins, + queryPluginsWithDebounced, + cancelQueryPluginsWithDebounced, + resetPlugins, + isLoading: true, + }) + + const { result } = renderHook(() => useMarketplaceAllPlugins([], '', false)) + + expect(useMarketplacePluginsByCollectionId).toHaveBeenCalledWith(undefined) + expect(queryPlugins).not.toHaveBeenCalled() + expect(queryPluginsWithDebounced).not.toHaveBeenCalled() + expect(cancelQueryPluginsWithDebounced).toHaveBeenCalled() + expect(resetPlugins).toHaveBeenCalled() + expect(result.current.plugins).toEqual([]) + expect(result.current.isLoading).toBe(false) + }) }) describe('useRefreshModel', () => { diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.ts b/web/app/components/header/account-setting/model-provider-page/hooks.ts index 27b84f81bb..84d138c1f2 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.ts @@ -267,22 +267,30 @@ export const useUpdateModelProviders = () => { return updateModelProviders } -export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: string) => { +export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: string, enabled = true) => { const exclude = useMemo(() => { return providers.map(provider => provider.provider.replace(/(.+)\/([^/]+)$/, '$1')) }, [providers]) const { plugins: collectionPlugins = [], isLoading: isCollectionLoading, - } = useMarketplacePluginsByCollectionId('__model-settings-pinned-models') + } = useMarketplacePluginsByCollectionId(enabled ? '__model-settings-pinned-models' : undefined) const { plugins, queryPlugins, queryPluginsWithDebounced, + cancelQueryPluginsWithDebounced = () => {}, + resetPlugins = () => {}, isLoading: isPluginsLoading, } = useMarketplacePlugins() useEffect(() => { + if (!enabled) { + cancelQueryPluginsWithDebounced() + resetPlugins() + return + } + if (searchText) { queryPluginsWithDebounced({ query: searchText, @@ -304,9 +312,12 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: sort_order: 'DESC', }) } - }, [queryPlugins, queryPluginsWithDebounced, searchText, exclude]) + }, [cancelQueryPluginsWithDebounced, enabled, queryPlugins, queryPluginsWithDebounced, resetPlugins, searchText, exclude]) const allPlugins = useMemo(() => { + if (!enabled) + return [] + const allPlugins = collectionPlugins.filter(plugin => !exclude.includes(plugin.plugin_id)) if (plugins?.length) { @@ -319,11 +330,11 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: } return allPlugins - }, [plugins, collectionPlugins, exclude]) + }, [enabled, plugins, collectionPlugins, exclude]) return { - plugins: searchText ? plugins : allPlugins, - isLoading: isCollectionLoading || isPluginsLoading, + plugins: enabled && searchText ? plugins : allPlugins, + isLoading: enabled && (isCollectionLoading || isPluginsLoading), } } diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx index 45218923d8..b373da1f6c 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx @@ -103,8 +103,18 @@ function PopupHarness(props: PopupTestProps) { ) } -const renderPopup = (ui: ReactElement) => renderWithSystemFeatures(ui, { - trialModels: mockTrialModels.current, +const renderPopup = ( + ui: ReactElement, + options: Parameters[1] = {}, +) => renderWithSystemFeatures(ui, { + ...options, + systemFeatures: options.systemFeatures === null + ? null + : { + enable_marketplace: true, + ...(options.systemFeatures ?? {}), + }, + trialModels: options.trialModels ?? mockTrialModels.current, }) const mockTrialCredits = vi.hoisted(() => ({ @@ -830,6 +840,26 @@ describe('Popup', () => { expect(screen.getByText(/modelProvider\.selector\.discoverMoreInMarketplace/))!.toBeInTheDocument() }) + it('should hide marketplace providers when marketplace is disabled', () => { + mockContextModelProviders.current = [makeContextProvider({ provider: 'test-openai' })] + + renderPopup( + , + { + systemFeatures: { enable_marketplace: false }, + }, + ) + + expect(screen.getByText('test-openai'))!.toBeInTheDocument() + expect(screen.queryByText('TestAnthropic')).not.toBeInTheDocument() + expect(screen.queryByText(/modelProvider\.selector\.fromMarketplace/)).not.toBeInTheDocument() + expect(screen.queryByText(/modelProvider\.selector\.discoverMoreInMarketplace/)).not.toBeInTheDocument() + expect(screen.queryByText(/common\.modelProvider\.selector\.install/)).not.toBeInTheDocument() + }) + it('should show installed marketplace providers without models when AI credits are available', () => { mockContextModelProviders.current = [makeContextProvider({ provider: 'test-anthropic', diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx index 7b5854d363..c7bd482370 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx @@ -3,7 +3,7 @@ import type { ModelSelectorPreviewPayload } from './popup-item' import type { ModelProviderQuotaGetPaid } from '@/types/model-provider' import { ComboboxList } from '@langgenius/dify-ui/combobox' import { createPreviewCardHandle, PreviewCard, PreviewCardContent } from '@langgenius/dify-ui/preview-card' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useTheme } from 'next-themes' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -15,6 +15,7 @@ import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/client' +import { systemFeaturesQueryOptions } from '@/service/system-features' import { useInstallPackageFromMarketPlace } from '@/service/use-plugins' import { CustomConfigurationStatusEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations' import { useLanguage, useMarketplaceAllPlugins } from '../hooks' @@ -55,10 +56,14 @@ function Popup({ const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false) const { setShowAccountSettingModal } = useModalContext() const { modelProviders } = useProviderContext() + const { data: enableMarketplace } = useSuspenseQuery({ + ...systemFeaturesQueryOptions(), + select: systemFeatures => systemFeatures.enable_marketplace, + }) const { plugins: allPlugins, isLoading: isMarketplacePluginsLoading, - } = useMarketplaceAllPlugins(modelProviders, '') + } = useMarketplaceAllPlugins(modelProviders, '', enableMarketplace) const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace() const { refreshPluginList } = useRefreshPluginList() const [installingProvider, setInstallingProvider] = useState(null) @@ -71,7 +76,7 @@ function Popup({ modelProviders.map(provider => [provider.provider, provider]), ), [modelProviders]) const aiCreditVisibleProviders = useMemo(() => { - if (isCreditsExhausted) + if (!enableMarketplace || isCreditsExhausted) return new Set() return new Set( @@ -79,8 +84,9 @@ function Popup({ .filter(provider => providerSupportsCredits(provider, trialModels)) .map(provider => provider.provider), ) - }, [isCreditsExhausted, modelProviders, trialModels]) - const showCreditsExhaustedAlert = isCreditsExhausted + }, [enableMarketplace, isCreditsExhausted, modelProviders, trialModels]) + const showCreditsExhaustedAlert = enableMarketplace + && isCreditsExhausted && modelProviders.some(provider => providerSupportsCredits(provider, trialModels)) const hasApiKeyFallback = modelProviders.some((provider) => { const isApiKeyActive = provider.custom_configuration?.status === CustomConfigurationStatusEnum.active @@ -88,7 +94,7 @@ function Popup({ }) const handleInstallPlugin = useCallback(async (key: ModelProviderQuotaGetPaid) => { - if (!allPlugins || isMarketplacePluginsLoading || installingProvider) + if (!enableMarketplace || !allPlugins || isMarketplacePluginsLoading || installingProvider) return const pluginId = providerKeyToPluginId[key] const plugin = allPlugins.find(p => p.plugin_id === pluginId) @@ -109,7 +115,7 @@ function Popup({ finally { setInstallingProvider(null) } - }, [allPlugins, installPackageFromMarketPlace, installingProvider, isMarketplacePluginsLoading, refreshPluginList]) + }, [allPlugins, enableMarketplace, installPackageFromMarketPlace, installingProvider, isMarketplacePluginsLoading, refreshPluginList]) const installedModelList = useMemo(() => { const modelMap = new Map(modelList.map(model => [model.provider, model])) @@ -154,9 +160,12 @@ function Popup({ }), [aiCreditVisibleProviders, defaultModel, inputValue, installedModelList, scopeFeatures, searchIndex]) const marketplaceProviders = useMemo(() => { + if (!enableMarketplace) + return [] + const installedProviders = new Set(modelProviders.map(provider => provider.provider)) return MODEL_PROVIDER_QUOTA_GET_PAID.filter(key => !installedProviders.has(key)) - }, [modelProviders]) + }, [enableMarketplace, modelProviders]) const handleOpenSettings = useCallback(() => { onHide() @@ -208,15 +217,17 @@ function Popup({ {scopeFeatures.length > 0 && ( )} - + {enableMarketplace && ( + + )}