fix(desktop): import cc-switch model ids from settings

Reproduce: pasted a cc-switch-style settings JSON with PROXY_MANAGED auth, claude-* request model env vars, and Qwen3Coder *_MODEL_NAME display labels into the Provider form test path.

Fix: derive the Provider main model from ANTHROPIC_MODEL or the Sonnet model env fallback, and keep *_MODEL_NAME values as display labels only. Treat both proxy-managed and PROXY_MANAGED placeholders as non-secret display placeholders.

Tested: bun test desktop/src/lib/__tests__/providerSettingsJson.test.ts

Tested: cd desktop && bun run test generalSettings.test.tsx

Tested: bun run check:desktop

Confidence: high

Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-06-23 18:05:16 +08:00
parent c907144788
commit be8b004c36
4 changed files with 135 additions and 6 deletions

View File

@ -1683,6 +1683,100 @@ describe('Settings > Providers tab', () => {
})
})
it('uses request model env instead of cc-switch display model names when testing pasted settings JSON', async () => {
providerStoreState.testConfig = vi.fn().mockResolvedValue({
connectivity: {
success: false,
latencyMs: 3,
error: '未配置供应商',
modelUsed: 'claude-sonnet-4-6',
httpStatus: 503,
},
})
providerStoreState.presets = [
{
id: 'deepseek',
name: 'DeepSeek',
baseUrl: 'https://api.deepseek.com/anthropic',
apiFormat: 'anthropic',
defaultModels: {
main: 'deepseek-v4-pro',
haiku: 'deepseek-v4-flash',
sonnet: 'deepseek-v4-pro',
opus: 'deepseek-v4-pro',
},
needsApiKey: true,
websiteUrl: '',
},
{
id: 'custom',
name: 'Custom',
baseUrl: '',
apiFormat: 'anthropic',
defaultModels: {
main: '',
haiku: '',
sonnet: '',
opus: '',
},
needsApiKey: true,
websiteUrl: '',
},
]
render(<Settings />)
fireEvent.click(screen.getByRole('button', { name: /Add Provider|添加服务商/i }))
const dialog = screen.getByRole('dialog')
const settingsTextarea = await waitFor(() => {
const textarea = dialog.querySelector('textarea')
expect(textarea?.value).toContain('"ANTHROPIC_MODEL"')
return textarea as HTMLTextAreaElement
})
fireEvent.change(settingsTextarea, {
target: {
value: JSON.stringify({
env: {
ANTHROPIC_API_KEY: 'PROXY_MANAGED',
ANTHROPIC_BASE_URL: 'http://127.0.0.1:15721',
ANTHROPIC_DEFAULT_FABLE_MODEL: 'Qwen3Coder',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5',
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: 'Qwen3Coder',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-8',
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: 'Qwen3Coder',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: 'Qwen3Coder',
},
}, null, 2),
},
})
await waitFor(() => {
expect(within(dialog).getByLabelText(/Main Model|主模型/i)).toHaveValue('claude-sonnet-4-6')
expect(within(dialog).getByLabelText(/Haiku Model/i)).toHaveValue('claude-haiku-4-5')
expect(within(dialog).getByLabelText(/Opus Model/i)).toHaveValue('claude-opus-4-8')
})
fireEvent.click(within(dialog).getByRole('button', { name: /Test Connection/i }))
await waitFor(() => {
expect(providerStoreState.testConfig).toHaveBeenCalledWith(expect.objectContaining({
baseUrl: 'http://127.0.0.1:15721',
apiKey: 'PROXY_MANAGED',
modelId: 'claude-sonnet-4-6',
authStrategy: 'api_key',
apiFormat: 'anthropic',
}))
})
expect(providerStoreState.testConfig).not.toHaveBeenCalledWith(expect.objectContaining({
modelId: 'Qwen3Coder',
}))
expect(providerStoreState.testConfig).not.toHaveBeenCalledWith(expect.objectContaining({
modelId: 'deepseek-v4-pro',
}))
})
it('keeps the provider form locked while save is in flight', async () => {
let resolveCreate!: (provider: SavedProvider) => void
providerStoreState.createProvider = vi.fn().mockImplementation(() => new Promise<SavedProvider>((resolve) => {

View File

@ -24,6 +24,22 @@ describe('provider settings JSON helpers', () => {
expect(masked.env.OTHER_VALUE).toBe('visible')
})
it('keeps cc-switch proxy-managed placeholders visible', () => {
const raw = JSON.stringify({
env: {
ANTHROPIC_API_KEY: 'PROXY_MANAGED',
ANTHROPIC_AUTH_TOKEN: 'proxy-managed',
OTHER_VALUE: 'visible',
},
})
const masked = JSON.parse(maskSettingsJsonSecrets(raw)) as { env: Record<string, string> }
expect(masked.env.ANTHROPIC_API_KEY).toBe('PROXY_MANAGED')
expect(masked.env.ANTHROPIC_AUTH_TOKEN).toBe('proxy-managed')
expect(masked.env.OTHER_VALUE).toBe('visible')
})
it('restores masked Anthropic API key env vars from their previous field values', () => {
const previousRaw = JSON.stringify({
env: {

View File

@ -1,6 +1,7 @@
export const API_KEY_JSON_PLACEHOLDER = '••••••••'
const API_KEY_JSON_KEYS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'] as const
const PROXY_MANAGED_PLACEHOLDERS = new Set(['proxy-managed', 'proxy_managed'])
const PROVIDER_SETTINGS_JSON_ENV_KEYS = new Set([
'ANTHROPIC_BASE_URL',
@ -41,12 +42,13 @@ function getEnvRecord(raw: string): Record<string, unknown> | null {
}
function isSecretDisplayValue(value: unknown): value is string {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
return (
typeof value === 'string' &&
value.trim() !== '' &&
value !== API_KEY_JSON_PLACEHOLDER &&
value !== '(your API key)' &&
value !== 'proxy-managed'
!PROXY_MANAGED_PLACEHOLDERS.has(normalized)
)
}

View File

@ -928,6 +928,27 @@ function normalizeModelMapping(models: ModelMapping): ModelMapping {
}
}
function readSettingsEnvString(env: Record<string, unknown>, key: string): string | undefined {
const value = env[key]
if (typeof value !== 'string') return undefined
const trimmed = value.trim()
return trimmed || undefined
}
function readModelMappingFromSettingsEnv(env: Record<string, unknown>): Partial<ModelMapping> {
const haiku = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_HAIKU_MODEL')
const sonnet = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_SONNET_MODEL')
const opus = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_OPUS_MODEL')
const main = readSettingsEnvString(env, 'ANTHROPIC_MODEL') ?? sonnet ?? haiku ?? opus
return {
...(main ? { main } : {}),
...(haiku ? { haiku } : {}),
...(sonnet ? { sonnet } : {}),
...(opus ? { opus } : {}),
}
}
function applyToolSearchEnv(
env: Record<string, unknown>,
apiFormat: ApiFormat,
@ -1854,11 +1875,7 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF
parsedContextWindows = {}
}
}
const newModels: Partial<ModelMapping> = {}
if (env.ANTHROPIC_MODEL) newModels.main = env.ANTHROPIC_MODEL
if (env.ANTHROPIC_DEFAULT_HAIKU_MODEL) newModels.haiku = env.ANTHROPIC_DEFAULT_HAIKU_MODEL
if (env.ANTHROPIC_DEFAULT_SONNET_MODEL) newModels.sonnet = env.ANTHROPIC_DEFAULT_SONNET_MODEL
if (env.ANTHROPIC_DEFAULT_OPUS_MODEL) newModels.opus = env.ANTHROPIC_DEFAULT_OPUS_MODEL
const newModels = readModelMappingFromSettingsEnv(env)
if (Object.keys(newModels).length > 0) {
setModels((prev) => {
const mergedModels = { ...prev, ...newModels }