From be8b004c36b3484dfdae070fcc1b6ff049f5838e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 23 Jun 2026 18:05:16 +0800 Subject: [PATCH] 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 --- .../src/__tests__/generalSettings.test.tsx | 94 +++++++++++++++++++ .../__tests__/providerSettingsJson.test.ts | 16 ++++ desktop/src/lib/providerSettingsJson.ts | 4 +- desktop/src/pages/Settings.tsx | 27 +++++- 4 files changed, 135 insertions(+), 6 deletions(-) diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index 0a6fb50e..40367de5 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -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() + + 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((resolve) => { diff --git a/desktop/src/lib/__tests__/providerSettingsJson.test.ts b/desktop/src/lib/__tests__/providerSettingsJson.test.ts index 6bd9795d..d6dd7300 100644 --- a/desktop/src/lib/__tests__/providerSettingsJson.test.ts +++ b/desktop/src/lib/__tests__/providerSettingsJson.test.ts @@ -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 } + + 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: { diff --git a/desktop/src/lib/providerSettingsJson.ts b/desktop/src/lib/providerSettingsJson.ts index 78bd34df..486908a2 100644 --- a/desktop/src/lib/providerSettingsJson.ts +++ b/desktop/src/lib/providerSettingsJson.ts @@ -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 | 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) ) } diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index 241b8f85..cfdc963a 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -928,6 +928,27 @@ function normalizeModelMapping(models: ModelMapping): ModelMapping { } } +function readSettingsEnvString(env: Record, 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): Partial { + 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, apiFormat: ApiFormat, @@ -1854,11 +1875,7 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF parsedContextWindows = {} } } - const newModels: Partial = {} - 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 }