diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index 43e5dafe..ed5e3514 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -242,6 +242,9 @@ pub struct ProviderMeta { /// - "openai_responses": OpenAI Responses API 格式,需要转换 #[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")] pub api_format: Option, + /// Claude 认证字段名("ANTHROPIC_AUTH_TOKEN" 或 "ANTHROPIC_API_KEY") + #[serde(rename = "apiKeyField", skip_serializing_if = "Option::is_none")] + pub api_key_field: Option, /// Prompt cache key for OpenAI-compatible endpoints. /// When set, injected into converted requests to improve cache hit rate. /// If not set, provider ID is used automatically during format conversion. diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx index c9430c09..6b8ceef3 100644 --- a/src/components/providers/forms/ClaudeFormFields.tsx +++ b/src/components/providers/forms/ClaudeFormFields.tsx @@ -10,7 +10,11 @@ import { } from "@/components/ui/select"; import EndpointSpeedTest from "./EndpointSpeedTest"; import { ApiKeySection, EndpointField } from "./shared"; -import type { ProviderCategory, ClaudeApiFormat } from "@/types"; +import type { + ProviderCategory, + ClaudeApiFormat, + ClaudeApiKeyField, +} from "@/types"; import type { TemplateValueConfig } from "@/config/claudeProviderPresets"; interface EndpointCandidate { @@ -68,6 +72,10 @@ interface ClaudeFormFieldsProps { // API Format (for third-party providers that use OpenAI Chat Completions format) apiFormat: ClaudeApiFormat; onApiFormatChange: (format: ClaudeApiFormat) => void; + + // Auth Field (ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY) + apiKeyField: ClaudeApiKeyField; + onApiKeyFieldChange: (field: ClaudeApiKeyField) => void; } export function ClaudeFormFields({ @@ -102,6 +110,8 @@ export function ClaudeFormFields({ speedTestEndpoints, apiFormat, onApiFormatChange, + apiKeyField, + onApiKeyFieldChange, }: ClaudeFormFieldsProps) { const { t } = useTranslation(); @@ -226,6 +236,40 @@ export function ClaudeFormFields({ )} + {/* 认证字段选择器 */} + {shouldShowModelSelector && ( +
+ + {t("providerForm.authField", { defaultValue: "认证字段" })} + + +

+ {t("providerForm.authFieldHint", { + defaultValue: "选择写入配置的认证环境变量名", + })} +

+
+ )} + {/* 模型选择器 */} {shouldShowModelSelector && (
diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 788e1c73..4bc2250b 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -14,6 +14,7 @@ import type { ProviderTestConfig, ProviderProxyConfig, ClaudeApiFormat, + ClaudeApiKeyField, } from "@/types"; import { providerPresets, @@ -244,6 +245,18 @@ export function ProviderForm({ [form], ); + const [localApiKeyField, setLocalApiKeyField] = useState( + () => { + if (appId !== "claude") return "ANTHROPIC_AUTH_TOKEN"; + if (initialData?.meta?.apiKeyField) return initialData.meta.apiKeyField; + // Infer from existing config env + const env = (initialData?.settingsConfig as Record) + ?.env as Record | undefined; + if (env?.ANTHROPIC_API_KEY !== undefined) return "ANTHROPIC_API_KEY"; + return "ANTHROPIC_AUTH_TOKEN"; + }, + ); + const { apiKey, handleApiKeyChange, @@ -254,6 +267,7 @@ export function ProviderForm({ selectedPresetId, category, appType: appId, + apiKeyField: appId === "claude" ? localApiKeyField : undefined, }); const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({ @@ -286,6 +300,30 @@ export function ProviderForm({ setLocalApiFormat(format); }, []); + const handleApiKeyFieldChange = useCallback( + (field: ClaudeApiKeyField) => { + const prev = localApiKeyField; + setLocalApiKeyField(field); + + // Swap the env key name in settingsConfig + try { + const raw = form.getValues("settingsConfig"); + const config = JSON.parse(raw || "{}"); + if (config?.env && prev in config.env) { + const value = config.env[prev]; + delete config.env[prev]; + config.env[field] = value; + const updated = JSON.stringify(config, null, 2); + form.setValue("settingsConfig", updated); + handleSettingsConfigChange(updated); + } + } catch { + // ignore parse errors during editing + } + }, + [localApiKeyField, form, handleSettingsConfigChange], + ); + const { codexAuth, codexConfig, @@ -844,6 +882,12 @@ export function ProviderForm({ appId === "claude" && category !== "official" ? localApiFormat : undefined, + apiKeyField: + appId === "claude" && + category !== "official" && + localApiKeyField !== "ANTHROPIC_AUTH_TOKEN" + ? localApiKeyField + : undefined, }; onSubmit(payload); @@ -1082,6 +1126,8 @@ export function ProviderForm({ setLocalApiFormat("anthropic"); } + setLocalApiKeyField(preset.apiKeyField ?? "ANTHROPIC_AUTH_TOKEN"); + form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", @@ -1287,6 +1333,8 @@ export function ProviderForm({ speedTestEndpoints={speedTestEndpoints} apiFormat={localApiFormat} onApiFormatChange={handleApiFormatChange} + apiKeyField={localApiKeyField} + onApiKeyFieldChange={handleApiKeyFieldChange} /> )} diff --git a/src/components/providers/forms/hooks/useApiKeyState.ts b/src/components/providers/forms/hooks/useApiKeyState.ts index 9101d2e3..8b4c8762 100644 --- a/src/components/providers/forms/hooks/useApiKeyState.ts +++ b/src/components/providers/forms/hooks/useApiKeyState.ts @@ -12,6 +12,7 @@ interface UseApiKeyStateProps { selectedPresetId: string | null; category?: ProviderCategory; appType?: string; + apiKeyField?: string; } /** @@ -24,6 +25,7 @@ export function useApiKeyState({ selectedPresetId, category, appType, + apiKeyField, }: UseApiKeyStateProps) { const [apiKey, setApiKey] = useState(() => { if (initialConfig) { @@ -58,7 +60,7 @@ export function useApiKeyState({ initialConfig || "{}", key.trim(), { - // 最佳实践:仅在“新增模式”且“非官方类别”时补齐缺失字段 + // 最佳实践:仅在"新增模式"且"非官方类别"时补齐缺失字段 // - 新增模式:selectedPresetId !== null // - 非官方类别:category !== undefined && category !== "official" // - 官方类别:不创建字段(UI 也会禁用输入框) @@ -68,12 +70,20 @@ export function useApiKeyState({ category !== undefined && category !== "official", appType, + apiKeyField, }, ); onConfigChange(configString); }, - [initialConfig, selectedPresetId, category, appType, onConfigChange], + [ + initialConfig, + selectedPresetId, + category, + appType, + apiKeyField, + onConfigChange, + ], ); const showApiKey = useCallback( diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f05e6c15..a60dd9cf 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -739,6 +739,10 @@ "apiFormatAnthropic": "Anthropic Messages (Native)", "apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)", "apiFormatOpenAIResponses": "OpenAI Responses API (Requires proxy)", + "authField": "Auth Field", + "authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN (Default)", + "authFieldApiKey": "ANTHROPIC_API_KEY", + "authFieldHint": "Select the authentication env variable name for the config", "apiHintResponses": "💡 Fill in OpenAI Responses API compatible service endpoint, avoid trailing slash", "anthropicDefaultHaikuModel": "Default Haiku Model", "anthropicDefaultSonnetModel": "Default Sonnet Model", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 02c2d2b0..6f67e6ff 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -739,6 +739,10 @@ "apiFormatAnthropic": "Anthropic Messages(ネイティブ)", "apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)", "apiFormatOpenAIResponses": "OpenAI Responses API(プロキシが必要)", + "authField": "認証フィールド", + "authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(デフォルト)", + "authFieldApiKey": "ANTHROPIC_API_KEY", + "authFieldHint": "設定に書き込む認証環境変数名を選択", "apiHintResponses": "💡 OpenAI Responses API 互換サービスのエンドポイントを入力してください。末尾にスラッシュを付けないでください", "anthropicDefaultHaikuModel": "既定 Haiku モデル", "anthropicDefaultSonnetModel": "既定 Sonnet モデル", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 3ff812d6..a8b1fc21 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -739,6 +739,10 @@ "apiFormatAnthropic": "Anthropic Messages (原生)", "apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)", "apiFormatOpenAIResponses": "OpenAI Responses API (需开启代理)", + "authField": "认证字段", + "authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(默认)", + "authFieldApiKey": "ANTHROPIC_API_KEY", + "authFieldHint": "选择写入配置的认证环境变量名", "apiHintResponses": "💡 填写兼容 OpenAI Responses API 的服务端点地址,不要以斜杠结尾", "anthropicDefaultHaikuModel": "Haiku 默认模型", "anthropicDefaultSonnetModel": "Sonnet 默认模型", diff --git a/src/types.ts b/src/types.ts index 5b1e2d69..2957b897 100644 --- a/src/types.ts +++ b/src/types.ts @@ -149,6 +149,8 @@ export interface ProviderMeta { // - "openai_chat": OpenAI Chat Completions 格式,需要格式转换 // - "openai_responses": OpenAI Responses API 格式,需要格式转换 apiFormat?: "anthropic" | "openai_chat" | "openai_responses"; + // Claude 认证字段名 + apiKeyField?: ClaudeApiKeyField; // Prompt cache key for OpenAI-compatible endpoints (improves cache hit rate) promptCacheKey?: string; } @@ -162,6 +164,9 @@ export type SkillSyncMethod = "auto" | "symlink" | "copy"; // - "openai_responses": OpenAI Responses API 格式,需要格式转换 export type ClaudeApiFormat = "anthropic" | "openai_chat" | "openai_responses"; +// Claude 认证字段类型 +export type ClaudeApiKeyField = "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY"; + // 主页面显示的应用配置 export interface VisibleApps { claude: boolean; diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index ba685f07..b35dcabe 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -295,9 +295,13 @@ export const hasApiKeyField = ( export const setApiKeyInConfig = ( jsonString: string, apiKey: string, - options: { createIfMissing?: boolean; appType?: string } = {}, + options: { + createIfMissing?: boolean; + appType?: string; + apiKeyField?: string; + } = {}, ): string => { - const { createIfMissing = false, appType } = options; + const { createIfMissing = false, appType, apiKeyField } = options; try { const config = JSON.parse(jsonString); @@ -337,13 +341,13 @@ export const setApiKeyInConfig = ( return JSON.stringify(config, null, 2); } - // Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则默认创建 AUTH_TOKEN 字段) + // Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则使用 apiKeyField 或默认 AUTH_TOKEN 字段) if ("ANTHROPIC_AUTH_TOKEN" in env) { env.ANTHROPIC_AUTH_TOKEN = apiKey; } else if ("ANTHROPIC_API_KEY" in env) { env.ANTHROPIC_API_KEY = apiKey; } else if (createIfMissing) { - env.ANTHROPIC_AUTH_TOKEN = apiKey; + env[apiKeyField ?? "ANTHROPIC_AUTH_TOKEN"] = apiKey; } else { return jsonString; }