cc-haha/desktop/src/lib/providerSettingsJson.ts
程序员阿江(Relakkes) 2c5c904c0a fix(provider): add experimental beta kill switch (#929)
Add a provider-level disableExperimentalBetas setting that writes CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 into managed provider runtime env, keeps stale parent env isolated, and exposes the toggle in desktop provider settings.

Tested: bun test src/server/__tests__/provider-runtime-env.test.ts --test-name-pattern "experimental betas|stale proxy"
Tested: bun test src/server/__tests__/providers.test.ts --test-name-pattern "experimental betas|POST /api/providers should create a provider|PUT /api/providers/:id should update a provider"
Tested: bun test src/server/__tests__/conversation-service.test.ts --test-name-pattern "experimental beta kill switch"
Tested: cd desktop && bun run test -- --run src/__tests__/generalSettings.test.tsx -t "experimental beta headers|Tool Search"
Tested: cd desktop && bun run test -- --run src/lib/__tests__/providerSettingsJson.test.ts
Tested: bun run check:server
Tested: cd desktop && bun run build
Not-tested: bun run verify
Confidence: high
Scope-risk: moderate
2026-07-02 21:01:14 +08:00

110 lines
3.5 KiB
TypeScript

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',
'ANTHROPIC_API_KEY',
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_SMALL_FAST_MODEL',
'ENABLE_TOOL_SEARCH',
'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS',
'CC_HAHA_OPENAI_OAUTH_PROVIDER',
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
'CLAUDE_CODE_MODEL_CONTEXT_WINDOWS',
'CLAUDE_CODE_SUBAGENT_MODEL',
'OPENAI_CODEX_OAUTH_FILE',
])
function getEnvRecord(raw: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(raw) as { env?: unknown }
if (!parsed.env || typeof parsed.env !== 'object' || Array.isArray(parsed.env)) {
return null
}
return parsed.env as Record<string, unknown>
} catch {
return 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)' &&
!PROXY_MANAGED_PLACEHOLDERS.has(normalized)
)
}
export function maskSettingsJsonSecrets(raw: string): string {
try {
const parsed = JSON.parse(raw) as { env?: Record<string, unknown> }
if (!parsed.env || typeof parsed.env !== 'object' || Array.isArray(parsed.env)) return raw
let changed = false
for (const key of API_KEY_JSON_KEYS) {
if (isSecretDisplayValue(parsed.env[key])) {
parsed.env[key] = API_KEY_JSON_PLACEHOLDER
changed = true
}
}
return changed ? JSON.stringify(parsed, null, 2) : raw
} catch {
return raw
}
}
export function restoreSettingsJsonSecrets<T>(
settings: T,
previousRaw: string,
fallbackApiKey = '',
): T {
if (!settings || typeof settings !== 'object') return settings
const parsed = settings as { env?: Record<string, unknown> }
if (!parsed.env || typeof parsed.env !== 'object' || Array.isArray(parsed.env)) return settings
const previousEnv = getEnvRecord(previousRaw)
const fallback = fallbackApiKey.trim()
for (const key of API_KEY_JSON_KEYS) {
if (parsed.env[key] !== API_KEY_JSON_PLACEHOLDER) continue
const previousValue = previousEnv?.[key]
if (isSecretDisplayValue(previousValue)) {
parsed.env[key] = previousValue
} else if (fallback) {
parsed.env[key] = fallback
}
}
return settings
}
export function stripProviderSettingsJsonEnv(
env: Record<string, string>,
extraManagedKeys: Iterable<string> = [],
): Record<string, string> {
const extraKeys = new Set(Array.from(extraManagedKeys, key => key.toUpperCase()))
return Object.fromEntries(
Object.entries(env).filter(([key]) => {
const upperKey = key.toUpperCase()
return !PROVIDER_SETTINGS_JSON_ENV_KEYS.has(upperKey) && !extraKeys.has(upperKey)
}),
)
}