mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-23 14:33:36 +08:00
fix(desktop): decouple provider creation from presets request #1078
This commit is contained in:
parent
7d272e0f96
commit
ba8917f1db
@ -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(),
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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')
|
||||||
},
|
},
|
||||||
|
|||||||
6
desktop/src/config/providerPresets.ts
Normal file
6
desktop/src/config/providerPresets.ts
Normal 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[]
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user