mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
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:
parent
c907144788
commit
be8b004c36
@ -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 () => {
|
it('keeps the provider form locked while save is in flight', async () => {
|
||||||
let resolveCreate!: (provider: SavedProvider) => void
|
let resolveCreate!: (provider: SavedProvider) => void
|
||||||
providerStoreState.createProvider = vi.fn().mockImplementation(() => new Promise<SavedProvider>((resolve) => {
|
providerStoreState.createProvider = vi.fn().mockImplementation(() => new Promise<SavedProvider>((resolve) => {
|
||||||
|
|||||||
@ -24,6 +24,22 @@ describe('provider settings JSON helpers', () => {
|
|||||||
expect(masked.env.OTHER_VALUE).toBe('visible')
|
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', () => {
|
it('restores masked Anthropic API key env vars from their previous field values', () => {
|
||||||
const previousRaw = JSON.stringify({
|
const previousRaw = JSON.stringify({
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export const API_KEY_JSON_PLACEHOLDER = '••••••••'
|
export const API_KEY_JSON_PLACEHOLDER = '••••••••'
|
||||||
|
|
||||||
const API_KEY_JSON_KEYS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'] as const
|
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([
|
const PROVIDER_SETTINGS_JSON_ENV_KEYS = new Set([
|
||||||
'ANTHROPIC_BASE_URL',
|
'ANTHROPIC_BASE_URL',
|
||||||
@ -41,12 +42,13 @@ function getEnvRecord(raw: string): Record<string, unknown> | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSecretDisplayValue(value: unknown): value is string {
|
function isSecretDisplayValue(value: unknown): value is string {
|
||||||
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
||||||
return (
|
return (
|
||||||
typeof value === 'string' &&
|
typeof value === 'string' &&
|
||||||
value.trim() !== '' &&
|
value.trim() !== '' &&
|
||||||
value !== API_KEY_JSON_PLACEHOLDER &&
|
value !== API_KEY_JSON_PLACEHOLDER &&
|
||||||
value !== '(your API key)' &&
|
value !== '(your API key)' &&
|
||||||
value !== 'proxy-managed'
|
!PROXY_MANAGED_PLACEHOLDERS.has(normalized)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
function applyToolSearchEnv(
|
||||||
env: Record<string, unknown>,
|
env: Record<string, unknown>,
|
||||||
apiFormat: ApiFormat,
|
apiFormat: ApiFormat,
|
||||||
@ -1854,11 +1875,7 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF
|
|||||||
parsedContextWindows = {}
|
parsedContextWindows = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newModels: Partial<ModelMapping> = {}
|
const newModels = readModelMappingFromSettingsEnv(env)
|
||||||
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
|
|
||||||
if (Object.keys(newModels).length > 0) {
|
if (Object.keys(newModels).length > 0) {
|
||||||
setModels((prev) => {
|
setModels((prev) => {
|
||||||
const mergedModels = { ...prev, ...newModels }
|
const mergedModels = { ...prev, ...newModels }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user