cc-haha/desktop/src/stores/providerStore.ts
程序员阿江(Relakkes) 1ff563d4b0 Expose ChatGPT Official model catalog
When ChatGPT Official is active, the model API now returns the OpenAI Codex catalog and reads or writes the selected GPT model from cc-haha managed settings. Desktop provider activation also recognizes the built-in provider id and resets the current model to the OpenAI default instead of looking for a saved provider record.

Constraint: ChatGPT Official is a built-in provider and is not present in providers.json providers[].

Rejected: Reuse the four-slot provider model list | it hides GPT-5.5 and misrepresents the OpenAI catalog as Anthropic slot names.

Confidence: high

Scope-risk: moderate

Tested: bun test src/server/__tests__/settings.test.ts

Tested: bun test src/server/__tests__/providers.test.ts --test-name-pattern "ChatGPT Official|getProviderForProxy"

Tested: bun test src/server/__tests__/conversation-service.test.ts --test-name-pattern "ChatGPT Official|session-scoped provider"

Tested: cd desktop && bun run test src/stores/providerStore.test.ts

Tested: git diff --check
2026-05-18 23:39:31 +08:00

184 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// desktop/src/stores/providerStore.ts
import { create } from 'zustand'
import { providersApi } from '../api/providers'
import { useChatStore } from './chatStore'
import { useSessionRuntimeStore } from './sessionRuntimeStore'
import { useSettingsStore } from './settingsStore'
import { OFFICIAL_DEFAULT_MODEL_ID } from '../constants/modelCatalog'
import {
OPENAI_OFFICIAL_DEFAULT_MODEL_ID,
OPENAI_OFFICIAL_PROVIDER_ID,
} from '../constants/openaiOfficialProvider'
import type {
SavedProvider,
CreateProviderInput,
UpdateProviderInput,
TestProviderConfigInput,
ProviderTestResult,
} from '../types/provider'
import type { ProviderPreset } from '../types/providerPreset'
import type { RuntimeSelection } from '../types/runtime'
type ProviderStore = {
providers: SavedProvider[]
activeId: string | null
hasLoadedProviders: boolean
presets: ProviderPreset[]
isLoading: boolean
isPresetsLoading: boolean
error: string | null
fetchProviders: () => Promise<void>
fetchPresets: () => Promise<void>
createProvider: (input: CreateProviderInput) => Promise<SavedProvider>
updateProvider: (id: string, input: UpdateProviderInput) => Promise<SavedProvider>
deleteProvider: (id: string) => Promise<void>
activateProvider: (id: string) => Promise<void>
activateOfficial: () => Promise<void>
testProvider: (id: string, overrides?: { baseUrl?: string; modelId?: string; apiFormat?: string; authStrategy?: string }) => Promise<ProviderTestResult>
testConfig: (input: TestProviderConfigInput) => Promise<ProviderTestResult>
}
function providerModelIds(provider: SavedProvider): Set<string> {
return new Set(
Object.values(provider.models)
.map((modelId) => modelId.trim())
.filter(Boolean),
)
}
function resolveRuntimeRefreshSelection(
provider: SavedProvider,
activeId: string | null,
currentSelection: RuntimeSelection | undefined,
): RuntimeSelection | null {
if (currentSelection?.providerId === provider.id) {
const modelIds = providerModelIds(provider)
return {
providerId: provider.id,
modelId: modelIds.has(currentSelection.modelId)
? currentSelection.modelId
: provider.models.main,
}
}
if (!currentSelection && activeId === provider.id) {
return {
providerId: provider.id,
modelId: provider.models.main,
}
}
return null
}
function refreshConnectedSessionsForProvider(provider: SavedProvider, activeId: string | null) {
const chatStore = useChatStore.getState()
const runtimeStore = useSessionRuntimeStore.getState()
for (const [sessionId, session] of Object.entries(chatStore.sessions)) {
if (session.connectionState !== 'connected' || session.chatState !== 'idle') {
continue
}
const selection = resolveRuntimeRefreshSelection(
provider,
activeId,
runtimeStore.selections[sessionId],
)
if (!selection) continue
runtimeStore.setSelection(sessionId, selection)
chatStore.setSessionRuntime(sessionId, selection)
}
}
export const useProviderStore = create<ProviderStore>((set, get) => ({
providers: [],
activeId: null,
hasLoadedProviders: false,
presets: [],
isLoading: false,
isPresetsLoading: false,
error: null,
fetchProviders: async () => {
set({ isLoading: true, error: null })
try {
const { providers, activeId } = await providersApi.list()
set({ providers, activeId, hasLoadedProviders: true, isLoading: false })
} catch (err) {
set({
isLoading: false,
error: err instanceof Error ? err.message : String(err),
})
}
},
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) => {
const { provider } = await providersApi.create(input)
await get().fetchProviders()
return provider
},
updateProvider: async (id, input) => {
const { provider } = await providersApi.update(id, input)
await get().fetchProviders()
refreshConnectedSessionsForProvider(provider, get().activeId)
return provider
},
deleteProvider: async (id) => {
await providersApi.delete(id)
await get().fetchProviders()
},
activateProvider: async (id) => {
await providersApi.activate(id)
await get().fetchProviders()
// 更新默认 provider 时,同步刷新默认 model避免 settings.json 里残留
// 旧 provider 的 model id 导致默认选择指向不存在的模型。
const settings = useSettingsStore.getState()
if (id === OPENAI_OFFICIAL_PROVIDER_ID) {
await settings.setModel(OPENAI_OFFICIAL_DEFAULT_MODEL_ID)
await settings.fetchAll()
return
}
const provider = get().providers.find((p) => p.id === id)
if (!provider) return
await settings.setModel(provider.models.main)
await settings.fetchAll()
},
activateOfficial: async () => {
await providersApi.activateOfficial()
await get().fetchProviders()
// 切回官方默认时同样重置 currentModel避免残留第三方 model id。
const settings = useSettingsStore.getState()
await settings.setModel(OFFICIAL_DEFAULT_MODEL_ID)
await settings.fetchAll()
},
testProvider: async (id, overrides?) => {
const { result } = await providersApi.test(id, overrides)
return result
},
testConfig: async (input) => {
const { result } = await providersApi.testConfig(input)
return result
},
}))