diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index cbeee21d0..0581e7461 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -235,6 +235,11 @@ pub struct ProviderMeta { /// - "openai_chat": OpenAI Chat Completions 格式,需要转换 #[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")] pub api_format: Option, + /// Claude 认证字段名(仅 Claude 供应商使用) + /// - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商 + /// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key + #[serde(rename = "apiKeyField", skip_serializing_if = "Option::is_none")] + pub api_key_field: Option, } impl ProviderManager { diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx index 699f5aa01..9dd750576 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 Key Field (ANTHROPIC_AUTH_TOKEN vs 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(); @@ -219,6 +229,41 @@ export function ClaudeFormFields({ )} + {/* 认证字段选择(仅非官方供应商显示) */} + {shouldShowModelSelector && ( +
+ + {t("providerForm.authField", { defaultValue: "认证字段" })} + + +

+ {t("providerForm.authFieldHint", { + defaultValue: + "大多数第三方供应商使用 Auth Token;少数供应商需要 API Key", + })} +

+
+ )} + {/* 模型选择器 */} {shouldShowModelSelector && (
diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 585d38666..03f4da197 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, @@ -240,6 +241,55 @@ export function ProviderForm({ mode: "onSubmit", }); + const [localApiFormat, setLocalApiFormat] = useState(() => { + if (appId !== "claude") return "anthropic"; + return initialData?.meta?.apiFormat ?? "anthropic"; + }); + + const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => { + setLocalApiFormat(format); + }, []); + + const [localApiKeyField, setLocalApiKeyField] = useState( + () => { + if (appId !== "claude") return "ANTHROPIC_AUTH_TOKEN"; + if (initialData?.meta?.apiKeyField) return initialData.meta.apiKeyField; + try { + const config = initialData?.settingsConfig; + if ( + config?.env && + (config.env as Record).ANTHROPIC_API_KEY !== + undefined + ) + return "ANTHROPIC_API_KEY"; + } catch {} + return "ANTHROPIC_AUTH_TOKEN"; + }, + ); + + const handleApiKeyFieldChange = useCallback( + (field: ClaudeApiKeyField) => { + setLocalApiKeyField(field); + try { + const config = JSON.parse(form.getValues("settingsConfig") || "{}") as { + env?: Record; + }; + const env = (config.env ?? {}) as Record; + const oldField = + field === "ANTHROPIC_API_KEY" + ? "ANTHROPIC_AUTH_TOKEN" + : "ANTHROPIC_API_KEY"; + if (oldField in env) { + env[field] = env[oldField]; + delete env[oldField]; + config.env = env; + form.setValue("settingsConfig", JSON.stringify(config, null, 2)); + } + } catch {} + }, + [form], + ); + const { apiKey, handleApiKeyChange, @@ -250,6 +300,7 @@ export function ProviderForm({ selectedPresetId, category, appType: appId, + apiKeyField: appId === "claude" ? localApiKeyField : undefined, }); const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({ @@ -273,15 +324,6 @@ export function ProviderForm({ onConfigChange: (config) => form.setValue("settingsConfig", config), }); - const [localApiFormat, setLocalApiFormat] = useState(() => { - if (appId !== "claude") return "anthropic"; - return initialData?.meta?.apiFormat ?? "anthropic"; - }); - - const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => { - setLocalApiFormat(format); - }, []); - const { codexAuth, codexConfig, @@ -775,6 +817,10 @@ export function ProviderForm({ appId === "claude" && category !== "official" ? localApiFormat : undefined, + apiKeyField: + appId === "claude" && category !== "official" + ? localApiKeyField + : undefined, }; onSubmit(payload); @@ -1012,6 +1058,12 @@ export function ProviderForm({ setLocalApiFormat("anthropic"); } + if (preset.apiKeyField) { + setLocalApiKeyField(preset.apiKeyField); + } else { + setLocalApiKeyField("ANTHROPIC_AUTH_TOKEN"); + } + form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", @@ -1216,6 +1268,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 9101d2e35..8b4c87626 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 a4b25a343..2602f0419 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -655,6 +655,10 @@ "apiFormatHint": "Select the input format for the provider's API", "apiFormatAnthropic": "Anthropic Messages (Native)", "apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)", + "authField": "Auth Field", + "authFieldAuthToken": "Auth Token (Default)", + "authFieldApiKey": "API Key", + "authFieldHint": "Most third-party providers use Auth Token; a few require API Key", "anthropicDefaultHaikuModel": "Default Haiku Model", "anthropicDefaultSonnetModel": "Default Sonnet Model", "anthropicDefaultOpusModel": "Default Opus Model", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 87a288ee9..f002b1aa2 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -655,6 +655,10 @@ "apiFormatHint": "プロバイダー API の入力フォーマットを選択", "apiFormatAnthropic": "Anthropic Messages(ネイティブ)", "apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)", + "authField": "認証フィールド", + "authFieldAuthToken": "Auth Token(デフォルト)", + "authFieldApiKey": "API Key", + "authFieldHint": "ほとんどのサードパーティプロバイダーは Auth Token を使用します。一部は API Key が必要です", "anthropicDefaultHaikuModel": "既定 Haiku モデル", "anthropicDefaultSonnetModel": "既定 Sonnet モデル", "anthropicDefaultOpusModel": "既定 Opus モデル", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index f4ffa5444..93176e5f3 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -655,6 +655,10 @@ "apiFormatHint": "选择供应商 API 的输入格式", "apiFormatAnthropic": "Anthropic Messages (原生)", "apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)", + "authField": "认证字段", + "authFieldAuthToken": "Auth Token (默认)", + "authFieldApiKey": "API Key", + "authFieldHint": "大多数第三方供应商使用 Auth Token;少数供应商需要 API Key", "anthropicDefaultHaikuModel": "Haiku 默认模型", "anthropicDefaultSonnetModel": "Sonnet 默认模型", "anthropicDefaultOpusModel": "Opus 默认模型", diff --git a/src/types.ts b/src/types.ts index a62bb2c8a..3a81ec3b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -145,6 +145,10 @@ export interface ProviderMeta { // - "anthropic": 原生 Anthropic Messages API 格式,直接透传 // - "openai_chat": OpenAI Chat Completions 格式,需要格式转换 apiFormat?: "anthropic" | "openai_chat"; + // Claude 认证字段名(仅 Claude 供应商使用) + // - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商 + // - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key + apiKeyField?: "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY"; } // Skill 同步方式 @@ -155,6 +159,11 @@ export type SkillSyncMethod = "auto" | "symlink" | "copy"; // - "openai_chat": OpenAI Chat Completions 格式,需要格式转换 export type ClaudeApiFormat = "anthropic" | "openai_chat"; +// Claude 认证字段类型 +// - "ANTHROPIC_AUTH_TOKEN": 大多数第三方/聚合供应商使用(默认) +// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key +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 8f73ec3bf..6d1cd90c5 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -135,9 +135,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); if (!config.env) { @@ -170,13 +174,13 @@ export const setApiKeyInConfig = ( return JSON.stringify(config, null, 2); } - // Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则默认创建 AUTH_TOKEN 字段) + // Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则使用 apiKeyField 指定的字段名) 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; }