From 5e18acb63dbc87147e5448fdac56f604a23f838a Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Wed, 11 Mar 2026 00:57:40 +0800 Subject: [PATCH] feat: provider auth style selector & edit UX improvements - Anthropic Third-party API: add API Key / Auth Token selector in connect dialog with proper extra_env injection and auth style detection - Edit mode: reuse PresetConnectDialog for matched providers instead of generic ProviderForm, showing only relevant fields consistent with connect experience - Edit mode: show masked key (dots) instead of empty field; switching auth style clears key and requires re-entry; switching back restores masked value - Edit mode: expose headers_json, env_overrides_json, notes in advanced section so all fields remain visible and editable - Edit mode: role_models_json merge logic preserves unseen roles (reasoning, small) while allowing clearing of visible mapping fields - Dialog: add max-h-[calc(100vh-4rem)] + overflow-y-auto to DialogContent base; remove overflow-hidden from preset dialog so scrolling actually works - Connected provider badge: detect ANTHROPIC_AUTH_TOKEN in extra_env to show correct "Auth Token" vs "API Key" label - i18n: add provider.authToken, provider.authStyle keys (en + zh) - Bump version to 0.31.0 Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 4 +- package.json | 2 +- src/components/settings/ProviderForm.tsx | 5 +- src/components/settings/ProviderManager.tsx | 356 +++++++++++++++++--- src/components/ui/dialog.tsx | 2 +- src/i18n/en.ts | 2 + src/i18n/zh.ts | 2 + 7 files changed, 316 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2680e80..45d2351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codepilot", - "version": "0.30.0", + "version": "0.31.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codepilot", - "version": "0.30.0", + "version": "0.31.0", "workspaces": [ "apps/*", "packages/*" diff --git a/package.json b/package.json index 9895fc1..83a2b56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codepilot", - "version": "0.30.0", + "version": "0.31.0", "private": true, "workspaces": ["apps/*", "packages/*"], "author": { diff --git a/src/components/settings/ProviderForm.tsx b/src/components/settings/ProviderForm.tsx index 54d49e7..8c412a9 100644 --- a/src/components/settings/ProviderForm.tsx +++ b/src/components/settings/ProviderForm.tsx @@ -95,7 +95,8 @@ export function ProviderForm({ setName(provider.name); setProviderType(provider.provider_type); setBaseUrl(provider.base_url); - setApiKey(""); + // Show masked key so user sees dots indicating a key exists + setApiKey(provider.api_key || ""); setExtraEnv(provider.extra_env || "{}"); setHeadersJson(provider.headers_json || "{}"); setEnvOverridesJson(provider.env_overrides_json || ""); @@ -202,7 +203,7 @@ export function ProviderForm({ } }; - const isMaskedKey = mode === "edit" && provider?.api_key?.startsWith("***"); + const isMaskedKey = mode === "edit" && apiKey?.startsWith("***"); return ( diff --git a/src/components/settings/ProviderManager.tsx b/src/components/settings/ProviderManager.tsx index c674fad..6ace6d4 100644 --- a/src/components/settings/ProviderManager.tsx +++ b/src/components/settings/ProviderManager.tsx @@ -6,6 +6,13 @@ import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Dialog, DialogContent, @@ -325,18 +332,30 @@ function PresetConnectDialog({ preset, open, onOpenChange, - onAdd, + onSave, + editProvider, }: { preset: QuickPreset | null; open: boolean; onOpenChange: (open: boolean) => void; - onAdd: (data: ProviderFormData) => Promise; + onSave: (data: ProviderFormData) => Promise; + /** When set, dialog operates in edit mode (pre-fills from existing provider) */ + editProvider?: ApiProvider | null; }) { + const isEdit = !!editProvider; const [apiKey, setApiKey] = useState(""); const [baseUrl, setBaseUrl] = useState(""); const [name, setName] = useState(""); const [extraEnv, setExtraEnv] = useState("{}"); const [modelName, setModelName] = useState(""); + // Auth style for anthropic-thirdparty: 'api_key' or 'auth_token' + const [authStyle, setAuthStyle] = useState<"api_key" | "auth_token">("api_key"); + // Track the initial auth style to detect changes + const [initialAuthStyle, setInitialAuthStyle] = useState<"api_key" | "auth_token">("api_key"); + // Edit-mode advanced fields + const [headersJson, setHeadersJson] = useState("{}"); + const [envOverridesJson, setEnvOverridesJson] = useState(""); + const [notes, setNotes] = useState(""); // Model mapping fields (sonnet/opus/haiku → actual API model IDs) const [mapSonnet, setMapSonnet] = useState(""); const [mapOpus, setMapOpus] = useState(""); @@ -347,21 +366,83 @@ function PresetConnectDialog({ const { t } = useTranslation(); const isZh = t('nav.chats') === '对话'; - // Reset form when dialog opens with a new preset + // Reset form when dialog opens useEffect(() => { if (!open || !preset) return; - setApiKey(""); - setBaseUrl(preset.base_url); - setName(preset.name); - setExtraEnv(preset.extra_env); - setModelName(""); - setMapSonnet(""); - setMapOpus(""); - setMapHaiku(""); setError(null); setSaving(false); - setShowAdvanced(false); - }, [open, preset]); + + if (isEdit && editProvider) { + // Edit mode — pre-fill from existing provider + setName(editProvider.name); + setBaseUrl(editProvider.base_url); + setApiKey(editProvider.api_key || ""); + setExtraEnv(editProvider.extra_env || preset.extra_env); + // Detect auth style from existing extra_env + try { + const env = JSON.parse(editProvider.extra_env || "{}"); + const detected = "ANTHROPIC_AUTH_TOKEN" in env ? "auth_token" as const : "api_key" as const; + setAuthStyle(detected); + setInitialAuthStyle(detected); + } catch { + setAuthStyle("api_key"); + setInitialAuthStyle("api_key"); + } + // Pre-fill advanced fields + setHeadersJson(editProvider.headers_json || "{}"); + setEnvOverridesJson(editProvider.env_overrides_json || ""); + setNotes(editProvider.notes || ""); + // Pre-fill model name from role_models_json + try { + const rm = JSON.parse(editProvider.role_models_json || "{}"); + setModelName(rm.default || ""); + setMapSonnet(rm.sonnet || ""); + setMapOpus(rm.opus || ""); + setMapHaiku(rm.haiku || ""); + } catch { + setModelName(""); + setMapSonnet(""); + setMapOpus(""); + setMapHaiku(""); + } + // Auto-expand advanced if there's meaningful data beyond preset defaults + const hasModelMapping = (() => { + try { + const rm = JSON.parse(editProvider.role_models_json || "{}"); + return !!(rm.sonnet || rm.opus || rm.haiku); + } catch { return false; } + })(); + const hasExtraEnvBeyondAuth = (() => { + try { + const env = JSON.parse(editProvider.extra_env || "{}"); + const meaningful = Object.keys(env).filter(k => + k !== "ANTHROPIC_API_KEY" && k !== "ANTHROPIC_AUTH_TOKEN" + ); + return meaningful.length > 0; + } catch { return false; } + })(); + const hasHeaders = editProvider.headers_json && editProvider.headers_json !== "{}"; + const hasEnvOverrides = !!editProvider.env_overrides_json; + const hasNotes = !!editProvider.notes; + setShowAdvanced(hasModelMapping || hasExtraEnvBeyondAuth || !!hasHeaders || hasEnvOverrides || hasNotes); + } else { + // Create mode — reset to preset defaults + setApiKey(""); + setBaseUrl(preset.base_url); + setName(preset.name); + setExtraEnv(preset.extra_env); + setModelName(""); + setAuthStyle("api_key"); + setInitialAuthStyle("api_key"); + setMapSonnet(""); + setMapOpus(""); + setMapHaiku(""); + setHeadersJson("{}"); + setEnvOverridesJson(""); + setNotes(""); + setShowAdvanced(false); + } + }, [open, preset, isEdit, editProvider]); if (!preset) return null; @@ -369,10 +450,42 @@ function PresetConnectDialog({ e.preventDefault(); setError(null); - const finalExtraEnv = extraEnv; - let roleModelsJson = "{}"; + // If auth style changed in edit mode, require a new key + if (isEdit && authStyle !== initialAuthStyle && (!apiKey || apiKey.startsWith("***"))) { + setError(isZh + ? '切换认证方式后需要重新输入密钥' + : 'Please re-enter the key after changing auth style'); + return; + } + + // For anthropic-thirdparty, inject the correct auth key into extra_env + // while preserving any other user-specified env vars (e.g. API_TIMEOUT_MS) + let finalExtraEnv = extraEnv; + if (preset.key === "anthropic-thirdparty") { + try { + const parsed = JSON.parse(extraEnv || "{}"); + // Remove both auth keys, then set the correct one + delete parsed["ANTHROPIC_API_KEY"]; + delete parsed["ANTHROPIC_AUTH_TOKEN"]; + if (authStyle === "auth_token") { + parsed["ANTHROPIC_AUTH_TOKEN"] = ""; + } else { + parsed["ANTHROPIC_API_KEY"] = ""; + } + finalExtraEnv = JSON.stringify(parsed); + } catch { + // If parse fails, fall back to simple replacement + finalExtraEnv = authStyle === "auth_token" + ? '{"ANTHROPIC_AUTH_TOKEN":""}' + : '{"ANTHROPIC_API_KEY":""}'; + } + } + // In edit mode, preserve existing role_models_json unless the user modifies mapping fields + let roleModelsJson = (isEdit && editProvider?.role_models_json) ? editProvider.role_models_json : "{}"; // Model mapping (sonnet/opus/haiku → actual API model IDs) + // Merge into existing roleModels to preserve roles not shown in this preset. + // If the preset exposes these fields and user cleared them all, remove those keys. if (preset.fields.includes("model_mapping")) { const hasAny = mapSonnet.trim() || mapOpus.trim() || mapHaiku.trim(); if (hasAny) { @@ -383,30 +496,51 @@ function PresetConnectDialog({ : 'Model mapping requires all 3 model names (Sonnet, Opus, Haiku)'); return; } + const existing = (() => { try { return JSON.parse(roleModelsJson); } catch { return {}; } })(); roleModelsJson = JSON.stringify({ + ...existing, sonnet: mapSonnet.trim(), opus: mapOpus.trim(), haiku: mapHaiku.trim(), }); + } else { + // All cleared — remove these keys from existing + const existing = (() => { try { return JSON.parse(roleModelsJson); } catch { return {}; } })(); + delete existing.sonnet; + delete existing.opus; + delete existing.haiku; + roleModelsJson = JSON.stringify(existing); } } - // Inject model name into role_models_json (preferred) instead of extra_env.ANTHROPIC_MODEL - if (preset.fields.includes("model_names") && modelName.trim()) { - roleModelsJson = JSON.stringify({ default: modelName.trim() }); + // Inject model name into role_models_json — merge, don't replace. + // If the preset exposes model_names and user cleared it, remove the default key. + if (preset.fields.includes("model_names")) { + const existing = (() => { try { return JSON.parse(roleModelsJson); } catch { return {}; } })(); + if (modelName.trim()) { + roleModelsJson = JSON.stringify({ ...existing, default: modelName.trim() }); + } else { + delete existing.default; + roleModelsJson = JSON.stringify(existing); + } } - // Validate extra_env JSON - try { - JSON.parse(finalExtraEnv); - } catch { - setError("Extra environment variables must be valid JSON"); - return; + // Validate JSON fields + for (const [label, val] of [ + ["Extra environment variables", finalExtraEnv], + ...(isEdit ? [["Headers", headersJson]] : []), + ] as const) { + if (val && val.trim()) { + try { JSON.parse(val); } catch { + setError(`${label} must be valid JSON`); + return; + } + } } setSaving(true); try { - await onAdd({ + await onSave({ name: name.trim() || preset.name, provider_type: preset.provider_type, protocol: preset.protocol, @@ -414,11 +548,13 @@ function PresetConnectDialog({ api_key: apiKey, extra_env: finalExtraEnv, role_models_json: roleModelsJson, - notes: "", + headers_json: isEdit ? headersJson.trim() || "{}" : undefined, + env_overrides_json: isEdit ? envOverridesJson.trim() || "" : undefined, + notes: isEdit ? notes.trim() : "", }); onOpenChange(false); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to add provider"); + setError(err instanceof Error ? err.message : (isEdit ? "Failed to update provider" : "Failed to add provider")); } finally { setSaving(false); } @@ -426,11 +562,11 @@ function PresetConnectDialog({ return ( - + {preset.icon} - {t('provider.connect')} {preset.name} + {isEdit ? t('provider.editProvider') : t('provider.connect')} {preset.name} {isZh ? preset.descriptionZh : preset.description} @@ -464,18 +600,50 @@ function PresetConnectDialog({ )} - {/* API Key */} + {/* API Key with optional auth style select */} {preset.fields.includes("api_key") && (
- - setApiKey(e.target.value)} - placeholder="sk-..." - className="text-sm font-mono" - autoFocus - /> + +
+ {preset.key === "anthropic-thirdparty" && ( + + )} + setApiKey(e.target.value)} + placeholder={authStyle === "auth_token" ? "token-..." : "sk-..."} + className="text-sm font-mono flex-1" + autoFocus + /> +
)} @@ -572,6 +740,42 @@ function PresetConnectDialog({ rows={3} /> + + {/* Edit-mode only: headers, env overrides, notes */} + {isEdit && ( + <> +
+ +