fix(web): respect marketplace feature flag in model selector (#36883)

This commit is contained in:
yyh 2026-06-01 12:11:58 +08:00 committed by GitHub
parent 07c0c4e7b1
commit bcd573e560
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 25 deletions

View File

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

View File

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

View File

@ -103,8 +103,18 @@ function PopupHarness(props: PopupTestProps) {
)
}
const renderPopup = (ui: ReactElement<PopupTestProps>) => renderWithSystemFeatures(ui, {
trialModels: mockTrialModels.current,
const renderPopup = (
ui: ReactElement<PopupTestProps>,
options: Parameters<typeof renderWithSystemFeatures>[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(
<PopupHarness
modelList={[makeModel({ provider: 'test-openai' })]}
onHide={vi.fn()}
/>,
{
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',

View File

@ -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<ModelProviderQuotaGetPaid | null>(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<string>()
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 && (
<CompatibleModelsNotice />
)}
<MarketplaceSection
marketplaceProviders={marketplaceProviders}
marketplaceCollapsed={marketplaceCollapsed}
installingProvider={installingProvider}
isMarketplacePluginsLoading={isMarketplacePluginsLoading}
theme={theme}
onMarketplaceCollapsedChange={setMarketplaceCollapsed}
onInstallPlugin={handleInstallPlugin}
/>
{enableMarketplace && (
<MarketplaceSection
marketplaceProviders={marketplaceProviders}
marketplaceCollapsed={marketplaceCollapsed}
installingProvider={installingProvider}
isMarketplacePluginsLoading={isMarketplacePluginsLoading}
theme={theme}
onMarketplaceCollapsedChange={setMarketplaceCollapsed}
onInstallPlugin={handleInstallPlugin}
/>
)}
</div>
</ModelSelectorScrollBody>
<PreviewCard handle={previewCardHandle}>