fix(provider): handle undefined provider in credential status and panel state

This commit is contained in:
CodingOnStar
2026-03-09 18:19:45 +08:00
parent 164cefc65c
commit ce0197b107
8 changed files with 55 additions and 16 deletions

View File

@ -53,4 +53,14 @@ describe('useCredentialStatus', () => {
expect(result.current.hasCredential).toBe(false)
expect(result.current.available_credentials).toBeUndefined()
})
it('handles undefined provider gracefully', () => {
const { result } = renderHook(() => useCredentialStatus(undefined))
expect(result.current.hasCredential).toBe(false)
expect(result.current.authorized).toBeFalsy()
expect(result.current.authRemoved).toBe(false)
expect(result.current.available_credentials).toBeUndefined()
expect(result.current.current_credential_id).toBeUndefined()
expect(result.current.current_credential_name).toBeUndefined()
})
})

View File

@ -3,12 +3,12 @@ import type {
} from '../../declarations'
import { useMemo } from 'react'
export const useCredentialStatus = (provider: ModelProvider) => {
export const useCredentialStatus = (provider: ModelProvider | undefined) => {
const {
current_credential_id,
current_credential_name,
available_credentials,
} = provider.custom_configuration
} = provider?.custom_configuration ?? {}
const hasCredential = !!available_credentials?.length
const authorized = current_credential_id && current_credential_name
const authRemoved = hasCredential && !current_credential_id && !current_credential_name

View File

@ -128,6 +128,18 @@ describe('PopupItem', () => {
})
})
it('should render nothing when provider is not found in modelProviders', () => {
mockUseProviderContext.mockReturnValue({
modelProviders: [],
})
const { container } = render(
<PopupItem model={makeModel()} onSelect={vi.fn()} onHide={vi.fn()} />,
)
expect(container.innerHTML).toBe('')
})
it('should call onSelect when clicking an active model', () => {
const onSelect = vi.fn()
render(<PopupItem model={makeModel()} onSelect={onSelect} onHide={vi.fn()} />)

View File

@ -59,7 +59,7 @@ const PopupItem: FC<PopupItemProps> = ({
const { modelProviders } = useProviderContext()
const updateModelList = useUpdateModelList()
const updateModelProviders = useUpdateModelProviders()
const currentProvider = modelProviders.find(provider => provider.provider === model.provider)!
const currentProvider = modelProviders.find(provider => provider.provider === model.provider)
const handleSelect = (provider: string, modelItem: ModelItem) => {
if (modelItem.status !== ModelStatusEnum.active)
return
@ -67,6 +67,8 @@ const PopupItem: FC<PopupItemProps> = ({
onSelect(provider, modelItem)
}
const handleOpenModelModal = () => {
if (!currentProvider)
return
setShowModelModal({
payload: {
currentProvider,
@ -96,6 +98,9 @@ const PopupItem: FC<PopupItemProps> = ({
onHide()
}, [onHide])
if (!currentProvider)
return null
return (
<div className="mb-1">
<div className="flex h-[22px] items-center justify-between px-3 text-xs font-medium text-text-tertiary">

View File

@ -137,11 +137,11 @@ const Popup: FC<PopupProps> = ({
}).filter(model => model.models.length > 0)
if (defaultModel?.provider) {
const selectedIndex = filtered.findIndex(m => m.provider === defaultModel.provider)
if (selectedIndex > 0) {
const [selected] = filtered.splice(selectedIndex, 1)
filtered.unshift(selected)
}
filtered.sort((a, b) => {
const aSelected = a.provider === defaultModel.provider ? 0 : 1
const bSelected = b.provider === defaultModel.provider ? 0 : 1
return aSelected - bSelected
})
}
return filtered

View File

@ -6,12 +6,12 @@ import { consoleQuery } from '@/service/client'
import { ConfigurationMethodEnum } from '../declarations'
import { useUpdateModelList, useUpdateModelProviders } from '../hooks'
export function useChangeProviderPriority(provider: ModelProvider) {
export function useChangeProviderPriority(provider: ModelProvider | undefined) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const updateModelList = useUpdateModelList()
const updateModelProviders = useUpdateModelProviders()
const providerName = provider.provider
const providerName = provider?.provider ?? ''
const modelProviderModelListQueryKey = consoleQuery.modelProviders.models.queryKey({
input: {
@ -31,9 +31,9 @@ export function useChangeProviderPriority(provider: ModelProvider) {
refetchType: 'none',
})
updateModelProviders()
provider.configurate_methods.forEach((method) => {
provider?.configurate_methods.forEach((method) => {
if (method === ConfigurationMethodEnum.predefinedModel)
provider.supported_model_types.forEach(modelType => updateModelList(modelType))
provider?.supported_model_types.forEach(modelType => updateModelList(modelType))
})
},
onError: () => {

View File

@ -183,6 +183,18 @@ describe('useCredentialPanelState', () => {
})
})
// Undefined provider
describe('Undefined provider', () => {
it('should return safe defaults when provider is undefined', () => {
const { result } = renderHook(() => useCredentialPanelState(undefined))
expect(result.current.priority).toBe('apiKeyOnly')
expect(result.current.supportsCredits).toBe(false)
expect(result.current.hasCredentials).toBe(false)
expect(result.current.credentialName).toBeUndefined()
})
})
// Derived metadata
describe('Derived metadata', () => {
it('should show priority switcher when credits supported and custom config active', () => {

View File

@ -70,7 +70,7 @@ function deriveVariant(
return 'api-required-add'
}
export function useCredentialPanelState(provider: ModelProvider): CredentialPanelState {
export function useCredentialPanelState(provider: ModelProvider | undefined): CredentialPanelState {
const { isExhausted, credits } = useTrialCredits()
const {
hasCredential,
@ -78,10 +78,10 @@ export function useCredentialPanelState(provider: ModelProvider): CredentialPane
current_credential_name,
} = useCredentialStatus(provider)
const systemConfig = provider.system_configuration
const preferredType = provider.preferred_provider_type
const systemConfig = provider?.system_configuration
const preferredType = provider?.preferred_provider_type
const supportsCredits = systemConfig.enabled && IS_CLOUD_EDITION
const supportsCredits = !!systemConfig?.enabled && IS_CLOUD_EDITION
const priority: UsagePriority = !supportsCredits
? 'apiKeyOnly'