fix(web): align model provider cache invalidation with oRPC keys

This commit is contained in:
yyh
2026-03-04 22:06:27 +08:00
parent d6d04ed657
commit 4d7a9bc798
6 changed files with 50 additions and 12 deletions

View File

@@ -25,6 +25,7 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useLocale } from '@/context/i18n'
import { useModalContextSelector } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { consoleQuery } from '@/service/client'
import {
fetchDefaultModal,
fetchModelList,
@@ -323,6 +324,7 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
export const useRefreshModel = () => {
const { eventEmitter } = useEventEmitterContextContext()
const queryClient = useQueryClient()
const updateModelProviders = useUpdateModelProviders()
const updateModelList = useUpdateModelList()
const handleRefreshModel = useCallback((
@@ -330,6 +332,11 @@ export const useRefreshModel = () => {
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
refreshModelList?: boolean,
) => {
queryClient.invalidateQueries({
queryKey: consoleQuery.modelProviders.models.key(),
refetchType: 'none',
})
updateModelProviders()
provider.supported_model_types.forEach((type) => {
@@ -345,7 +352,7 @@ export const useRefreshModel = () => {
if (CustomConfigurationModelFixedFields?.__model_type)
updateModelList(CustomConfigurationModelFixedFields.__model_type)
}
}, [eventEmitter, updateModelList, updateModelProviders])
}, [eventEmitter, queryClient, updateModelList, updateModelProviders])
return {
handleRefreshModel,

View File

@@ -1,4 +1,5 @@
import type { ModelProvider } from '../declarations'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { changeModelProviderPriority } from '@/service/common'
import { ConfigurationMethodEnum } from '../declarations'
@@ -71,6 +72,21 @@ vi.mock('@/app/components/header/indicator', () => ({
default: ({ color }: { color: string }) => <div data-testid="indicator">{color}</div>,
}))
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },
},
})
const renderWithQueryClient = (provider: ModelProvider) => {
const queryClient = createTestQueryClient()
return render(
<QueryClientProvider client={queryClient}>
<CredentialPanel provider={provider} />
</QueryClientProvider>,
)
}
describe('CredentialPanel', () => {
const mockProvider: ModelProvider = {
provider: 'test-provider',
@@ -94,7 +110,7 @@ describe('CredentialPanel', () => {
})
it('should show credential name and configuration actions', () => {
render(<CredentialPanel provider={mockProvider} />)
renderWithQueryClient(mockProvider)
expect(screen.getByText('test-credential')).toBeInTheDocument()
expect(screen.getByTestId('config-provider')).toBeInTheDocument()
@@ -103,7 +119,7 @@ describe('CredentialPanel', () => {
it('should show unauthorized status label when credential is missing', () => {
mockCredentialStatus.hasCredential = false
render(<CredentialPanel provider={mockProvider} />)
renderWithQueryClient(mockProvider)
expect(screen.getByText(/modelProvider\.auth\.unAuthorized/)).toBeInTheDocument()
})
@@ -111,7 +127,7 @@ describe('CredentialPanel', () => {
it('should show removed credential label and priority tip for custom preference', () => {
mockCredentialStatus.authorized = false
mockCredentialStatus.authRemoved = true
render(<CredentialPanel provider={{ ...mockProvider, preferred_provider_type: 'custom' } as ModelProvider} />)
renderWithQueryClient({ ...mockProvider, preferred_provider_type: 'custom' } as ModelProvider)
expect(screen.getByText(/modelProvider\.auth\.authRemoved/)).toBeInTheDocument()
expect(screen.getByTestId('priority-use-tip')).toBeInTheDocument()
@@ -120,7 +136,7 @@ describe('CredentialPanel', () => {
it('should change priority and refresh related data after success', async () => {
const mockChangePriority = changeModelProviderPriority as ReturnType<typeof vi.fn>
mockChangePriority.mockResolvedValue({ result: 'success' })
render(<CredentialPanel provider={mockProvider} />)
renderWithQueryClient(mockProvider)
fireEvent.click(screen.getByTestId('priority-selector'))
@@ -138,7 +154,7 @@ describe('CredentialPanel', () => {
...mockProvider,
provider_credential_schema: null,
} as unknown as ModelProvider
render(<CredentialPanel provider={providerNoSchema} />)
renderWithQueryClient(providerNoSchema)
expect(screen.getByTestId('priority-selector')).toBeInTheDocument()
expect(screen.queryByTestId('config-provider')).not.toBeInTheDocument()
})

View File

@@ -1,6 +1,7 @@
import type {
ModelProvider,
} from '../declarations'
import { useQueryClient } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useToastContext } from '@/app/components/base/toast'
@@ -9,6 +10,7 @@ import { useCredentialStatus } from '@/app/components/header/account-setting/mod
import Indicator from '@/app/components/header/indicator'
import { IS_CLOUD_EDITION } from '@/config'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { consoleQuery } from '@/service/client'
import { changeModelProviderPriority } from '@/service/common'
import { cn } from '@/utils/classnames'
import {
@@ -34,6 +36,7 @@ const CredentialPanel = ({
const { t } = useTranslation()
const { notify } = useToastContext()
const { eventEmitter } = useEventEmitterContextContext()
const queryClient = useQueryClient()
const updateModelList = useUpdateModelList()
const updateModelProviders = useUpdateModelProviders()
const customConfig = provider.custom_configuration
@@ -60,6 +63,10 @@ const CredentialPanel = ({
})
if (res.result === 'success') {
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
queryClient.invalidateQueries({
queryKey: consoleQuery.modelProviders.models.key(),
refetchType: 'none',
})
updateModelProviders()
configurateMethods.forEach((method) => {
@@ -82,7 +89,7 @@ const CredentialPanel = ({
return t('modelProvider.auth.authRemoved', { ns: 'common' })
return ''
}, [authorized, authRemoved, current_credential_name, hasCredential])
}, [authorized, authRemoved, current_credential_name, hasCredential, t])
const color = useMemo(() => {
if (authRemoved || !hasCredential)

View File

@@ -120,13 +120,13 @@ describe('ProviderAddedCard', () => {
// Explicitly re-find and click to re-open
fireEvent.click(screen.getByTestId('show-models-button'))
expect(await screen.findByTestId('model-list')).toBeInTheDocument()
expect(mockFetchModelProviderModels).toHaveBeenCalledTimes(1) // Should not fetch again
expect(mockFetchModelProviderModels).toHaveBeenCalledTimes(2) // Re-open fetches again with default stale/gc behavior
// Refresh list from ModelList
const refreshBtn = screen.getByRole('button', { name: 'refresh list' })
fireEvent.click(refreshBtn)
await waitFor(() => {
expect(mockFetchModelProviderModels).toHaveBeenCalledTimes(2)
expect(mockFetchModelProviderModels).toHaveBeenCalledTimes(3)
})
})

View File

@@ -83,14 +83,14 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
const showCustomModelActions = supportsCustomizableModel && isCurrentWorkspaceManager
const refreshModelList = useCallback((targetProviderName: string) => {
if (targetProviderName !== currentProviderName || loading)
if (targetProviderName !== currentProviderName)
return
if (collapsed)
setCollapsed(false)
refetchModelList().catch(() => {})
}, [collapsed, currentProviderName, loading, refetchModelList])
}, [collapsed, currentProviderName, refetchModelList])
const handleOpenModelList = useCallback(() => {
if (loading)

View File

@@ -1,4 +1,5 @@
import type { ModelItem, ModelProvider } from '../declarations'
import { useQueryClient } from '@tanstack/react-query'
import { useDebounceFn } from 'ahooks'
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@@ -9,6 +10,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
import { consoleQuery } from '@/service/client'
import { disableModel, enableModel } from '@/service/common'
import { cn } from '@/utils/classnames'
import { ModelStatusEnum } from '../declarations'
@@ -30,6 +32,7 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad
const { plan } = useProviderContext()
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const { isCurrentWorkspaceManager } = useAppContext()
const queryClient = useQueryClient()
const updateModelList = useUpdateModelList()
const toggleModelEnablingStatus = useCallback(async (enabled: boolean) => {
@@ -37,9 +40,14 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad
await enableModel(`/workspaces/current/model-providers/${provider.provider}/models/enable`, { model: model.model, model_type: model.model_type })
else
await disableModel(`/workspaces/current/model-providers/${provider.provider}/models/disable`, { model: model.model, model_type: model.model_type })
queryClient.invalidateQueries({
queryKey: consoleQuery.modelProviders.models.key(),
refetchType: 'none',
})
updateModelList(model.model_type)
onChange?.(provider.provider)
}, [model.model, model.model_type, onChange, provider.provider, updateModelList])
}, [model.model, model.model_type, onChange, provider.provider, queryClient, updateModelList])
const { run: debouncedToggleModelEnablingStatus } = useDebounceFn(toggleModelEnablingStatus, { wait: 500 })