fix(desktop): decouple provider creation from presets request #1078

This commit is contained in:
程序员阿江(Relakkes) 2026-07-20 20:38:51 +08:00
parent 7d272e0f96
commit ba8917f1db
11 changed files with 43 additions and 47 deletions

View File

@ -24,9 +24,7 @@ vi.mock('../stores/providerStore', () => ({
activeId: null, activeId: null,
presets: [], presets: [],
isLoading: false, isLoading: false,
isPresetsLoading: false,
fetchProviders: vi.fn(), fetchProviders: vi.fn(),
fetchPresets: vi.fn(),
deleteProvider: vi.fn(), deleteProvider: vi.fn(),
activateProvider: vi.fn(), activateProvider: vi.fn(),
activateOfficial: vi.fn(), activateOfficial: vi.fn(),

View File

@ -94,9 +94,7 @@ vi.mock('../stores/providerStore', () => ({
hasLoadedProviders: true, hasLoadedProviders: true,
presets: [], presets: [],
isLoading: false, isLoading: false,
isPresetsLoading: false,
fetchProviders: vi.fn(), fetchProviders: vi.fn(),
fetchPresets: vi.fn(),
deleteProvider: vi.fn(), deleteProvider: vi.fn(),
activateProvider: vi.fn(), activateProvider: vi.fn(),
activateOfficial: vi.fn(), activateOfficial: vi.fn(),

View File

@ -40,9 +40,7 @@ const providerStoreState = {
hasLoadedProviders: true, hasLoadedProviders: true,
presets: [] as ProviderPreset[], presets: [] as ProviderPreset[],
isLoading: false, isLoading: false,
isPresetsLoading: false,
fetchProviders: vi.fn(), fetchProviders: vi.fn(),
fetchPresets: vi.fn(),
deleteProvider: MOCK_DELETE_PROVIDER, deleteProvider: MOCK_DELETE_PROVIDER,
activateProvider: vi.fn(), activateProvider: vi.fn(),
activateOfficial: vi.fn(), activateOfficial: vi.fn(),
@ -199,9 +197,7 @@ describe('Settings > General tab', () => {
providerStoreState.hasLoadedProviders = true providerStoreState.hasLoadedProviders = true
providerStoreState.presets = [] providerStoreState.presets = []
providerStoreState.isLoading = false providerStoreState.isLoading = false
providerStoreState.isPresetsLoading = false
providerStoreState.fetchProviders = vi.fn() providerStoreState.fetchProviders = vi.fn()
providerStoreState.fetchPresets = vi.fn()
providerStoreState.activateProvider = vi.fn() providerStoreState.activateProvider = vi.fn()
providerStoreState.activateOfficial = vi.fn() providerStoreState.activateOfficial = vi.fn()
providerStoreState.testProvider = vi.fn() providerStoreState.testProvider = vi.fn()
@ -1687,6 +1683,21 @@ describe('Settings > Providers tab', () => {
expect(MOCK_DELETE_PROVIDER).toHaveBeenCalledWith('provider-1') expect(MOCK_DELETE_PROVIDER).toHaveBeenCalledWith('provider-1')
}) })
it('keeps custom provider creation available when presets are unavailable', () => {
providerStoreState.presets = []
render(<Settings />)
const addButton = screen.getByRole('button', { name: /Add Provider/i })
expect(addButton).toBeEnabled()
fireEvent.click(addButton)
const dialog = screen.getByRole('dialog')
expect(within(dialog).getByLabelText(/Name/i)).toHaveValue('Custom')
expect(within(dialog).getByLabelText(/Base URL/i)).toBeEnabled()
})
it('uses the shared dropdown for API format in the provider form', () => { it('uses the shared dropdown for API format in the provider form', () => {
providerStoreState.presets = [ providerStoreState.presets = [
{ {

View File

@ -25,9 +25,7 @@ vi.mock('../stores/providerStore', () => ({
activeId: null, activeId: null,
presets: [], presets: [],
isLoading: false, isLoading: false,
isPresetsLoading: false,
fetchProviders: vi.fn(), fetchProviders: vi.fn(),
fetchPresets: vi.fn(),
deleteProvider: vi.fn(), deleteProvider: vi.fn(),
activateProvider: vi.fn(), activateProvider: vi.fn(),
activateOfficial: vi.fn(), activateOfficial: vi.fn(),

View File

@ -21,9 +21,7 @@ vi.mock('../stores/providerStore', () => ({
activeId: null, activeId: null,
presets: [], presets: [],
isLoading: false, isLoading: false,
isPresetsLoading: false,
fetchProviders: vi.fn(), fetchProviders: vi.fn(),
fetchPresets: vi.fn(),
deleteProvider: vi.fn(), deleteProvider: vi.fn(),
activateProvider: vi.fn(), activateProvider: vi.fn(),
activateOfficial: vi.fn(), activateOfficial: vi.fn(),

View File

@ -8,13 +8,11 @@ import type {
TestProviderConfigInput, TestProviderConfigInput,
ProviderTestResult, ProviderTestResult,
} from '../types/provider' } from '../types/provider'
import type { ProviderPreset } from '../types/providerPreset'
type ProvidersResponse = { providers: SavedProvider[]; activeId: string | null } type ProvidersResponse = { providers: SavedProvider[]; activeId: string | null }
type ProvidersListResponse = ProvidersResponse & { providerOrder?: string[] } type ProvidersListResponse = ProvidersResponse & { providerOrder?: string[] }
type ProvidersReorderResponse = { providers: SavedProvider[]; providerOrder?: string[] } type ProvidersReorderResponse = { providers: SavedProvider[]; providerOrder?: string[] }
type ProviderResponse = { provider: SavedProvider } type ProviderResponse = { provider: SavedProvider }
type PresetsResponse = { presets: ProviderPreset[] }
type TestResultResponse = { result: ProviderTestResult } type TestResultResponse = { result: ProviderTestResult }
type AuthStatusResponse = { type AuthStatusResponse = {
hasAuth: boolean hasAuth: boolean
@ -27,10 +25,6 @@ export const providersApi = {
return api.get<ProvidersListResponse>('/api/providers') return api.get<ProvidersListResponse>('/api/providers')
}, },
presets() {
return api.get<PresetsResponse>('/api/providers/presets')
},
authStatus() { authStatus() {
return api.get<AuthStatusResponse>('/api/providers/auth-status') return api.get<AuthStatusResponse>('/api/providers/auth-status')
}, },

View File

@ -0,0 +1,6 @@
import providerPresetsJson from '../../../src/server/config/providerPresets.json'
import type { ProviderPreset } from '../types/providerPreset'
// Presets ship with the desktop bundle. Provider creation must remain available
// even when the local HTTP control plane is temporarily unavailable.
export const BUNDLED_PROVIDER_PRESETS = providerPresetsJson as ProviderPreset[]

View File

@ -361,9 +361,7 @@ function ProviderSettings() {
hasLoadedProviders, hasLoadedProviders,
presets, presets,
isLoading, isLoading,
isPresetsLoading,
fetchProviders, fetchProviders,
fetchPresets,
deleteProvider, deleteProvider,
reorderProviders, reorderProviders,
activateProvider, activateProvider,
@ -388,8 +386,7 @@ function ProviderSettings() {
useEffect(() => { useEffect(() => {
void fetchProviders() void fetchProviders()
void fetchPresets() }, [fetchProviders])
}, [fetchPresets, fetchProviders])
const presetMap = useMemo( const presetMap = useMemo(
() => new Map(presets.map((preset) => [preset.id, preset])), () => new Map(presets.map((preset) => [preset.id, preset])),
@ -462,7 +459,7 @@ function ProviderSettings() {
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">{t('settings.providers.title')}</h2> <h2 className="text-base font-semibold text-[var(--color-text-primary)]">{t('settings.providers.title')}</h2>
<p className="text-sm text-[var(--color-text-tertiary)] mt-0.5">{t('settings.providers.description')}</p> <p className="text-sm text-[var(--color-text-tertiary)] mt-0.5">{t('settings.providers.description')}</p>
</div> </div>
<Button size="sm" onClick={() => setShowCreateModal(true)} disabled={isPresetsLoading || presets.length === 0}> <Button size="sm" onClick={() => setShowCreateModal(true)}>
<span className="material-symbols-outlined text-[16px]">add</span> <span className="material-symbols-outlined text-[16px]">add</span>
{t('settings.providers.addProvider')} {t('settings.providers.addProvider')}
</Button> </Button>
@ -1211,21 +1208,18 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF
const fetchSettings = useSettingsStore((s) => s.fetchAll) const fetchSettings = useSettingsStore((s) => s.fetchAll)
const t = useTranslation() const t = useTranslation()
const availablePresets = presets.filter((p) => p.id !== 'official') const fallbackPreset = buildFallbackPreset(provider)
const loadedPresets = presets.filter((p) => p.id !== 'official')
const availablePresets = loadedPresets.length > 0 ? loadedPresets : [fallbackPreset]
const regularPresets = availablePresets.filter((p) => !p.featured) const regularPresets = availablePresets.filter((p) => !p.featured)
const featuredPresets = availablePresets.filter((p) => p.featured) const featuredPresets = availablePresets.filter((p) => p.featured)
const presetDefaultEnvKeys = useMemo( const presetDefaultEnvKeys = useMemo(
() => presets.flatMap((preset) => Object.keys(preset.defaultEnv ?? {})), () => presets.flatMap((preset) => Object.keys(preset.defaultEnv ?? {})),
[presets], [presets],
) )
const fallbackPreset = provider const initialPreset = provider
? buildFallbackPreset(provider) ? availablePresets.find((p) => p.id === provider.presetId) ?? fallbackPreset
: requirePreset(availablePresets[availablePresets.length - 1]) : availablePresets[0] ?? fallbackPreset
const initialPreset = requirePreset(
provider
? availablePresets.find((p) => p.id === provider.presetId) ?? fallbackPreset
: availablePresets[0] ?? fallbackPreset,
)
const initialModels = stripModel1mMarkers(provider?.models ?? initialPreset.defaultModels) const initialModels = stripModel1mMarkers(provider?.models ?? initialPreset.defaultModels)
const initialModel1mSupport = getInitialModel1mSupport( const initialModel1mSupport = getInitialModel1mSupport(
provider?.models ?? initialPreset.defaultModels, provider?.models ?? initialPreset.defaultModels,

View File

@ -12,7 +12,6 @@ const {
} = vi.hoisted(() => ({ } = vi.hoisted(() => ({
providersApiMock: { providersApiMock: {
list: vi.fn(), list: vi.fn(),
presets: vi.fn(),
authStatus: vi.fn(), authStatus: vi.fn(),
getSettings: vi.fn(), getSettings: vi.fn(),
updateSettings: vi.fn(), updateSettings: vi.fn(),
@ -88,6 +87,17 @@ function makeProvider(overrides: Partial<SavedProvider> = {}): SavedProvider {
} }
} }
describe('providerStore presets', () => {
it('starts with the provider presets bundled into the desktop app', async () => {
const { useProviderStore } = await import('./providerStore')
expect(useProviderStore.getState().presets).toEqual(expect.arrayContaining([
expect.objectContaining({ id: 'custom' }),
expect.objectContaining({ id: 'deepseek' }),
]))
})
})
describe('providerStore runtime refresh', () => { describe('providerStore runtime refresh', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()

View File

@ -15,6 +15,7 @@ import {
GROK_OFFICIAL_DEFAULT_MODEL_ID, GROK_OFFICIAL_DEFAULT_MODEL_ID,
GROK_OFFICIAL_PROVIDER_ID, GROK_OFFICIAL_PROVIDER_ID,
} from '../constants/grokOfficialProvider' } from '../constants/grokOfficialProvider'
import { BUNDLED_PROVIDER_PRESETS } from '../config/providerPresets'
import type { import type {
SavedProvider, SavedProvider,
CreateProviderInput, CreateProviderInput,
@ -32,11 +33,9 @@ type ProviderStore = {
hasLoadedProviders: boolean hasLoadedProviders: boolean
presets: ProviderPreset[] presets: ProviderPreset[]
isLoading: boolean isLoading: boolean
isPresetsLoading: boolean
error: string | null error: string | null
fetchProviders: () => Promise<void> fetchProviders: () => Promise<void>
fetchPresets: () => Promise<void>
createProvider: (input: CreateProviderInput) => Promise<SavedProvider> createProvider: (input: CreateProviderInput) => Promise<SavedProvider>
updateProvider: (id: string, input: UpdateProviderInput) => Promise<SavedProvider> updateProvider: (id: string, input: UpdateProviderInput) => Promise<SavedProvider>
deleteProvider: (id: string) => Promise<void> deleteProvider: (id: string) => Promise<void>
@ -164,9 +163,8 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
providerOrder: [...BUILT_IN_PROVIDER_IDS], providerOrder: [...BUILT_IN_PROVIDER_IDS],
activeId: null, activeId: null,
hasLoadedProviders: false, hasLoadedProviders: false,
presets: [], presets: BUNDLED_PROVIDER_PRESETS,
isLoading: false, isLoading: false,
isPresetsLoading: false,
error: null, error: null,
fetchProviders: async () => { fetchProviders: async () => {
@ -188,16 +186,6 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
} }
}, },
fetchPresets: async () => {
set({ isPresetsLoading: true, error: null })
try {
const { presets } = await providersApi.presets()
set({ presets, isPresetsLoading: false })
} catch (err) {
set({ isPresetsLoading: false, error: err instanceof Error ? err.message : String(err) })
}
},
createProvider: async (input) => { createProvider: async (input) => {
const { provider } = await providersApi.create(input) const { provider } = await providersApi.create(input)
await get().fetchProviders() await get().fetchProviders()

View File

@ -6,6 +6,7 @@
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"isolatedModules": true, "isolatedModules": true,
"moduleDetection": "force", "moduleDetection": "force",