mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-29 23:29:10 +08:00
feat: restore Claude provider auth field selector (AUTH_TOKEN / API_KEY)
This commit is contained in:
@@ -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<String>,
|
||||
/// Claude 认证字段名("ANTHROPIC_AUTH_TOKEN" 或 "ANTHROPIC_API_KEY")
|
||||
#[serde(rename = "apiKeyField", skip_serializing_if = "Option::is_none")]
|
||||
pub api_key_field: Option<String>,
|
||||
/// 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.
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 认证字段选择器 */}
|
||||
{shouldShowModelSelector && (
|
||||
<div className="space-y-2">
|
||||
<FormLabel>
|
||||
{t("providerForm.authField", { defaultValue: "认证字段" })}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={apiKeyField}
|
||||
onValueChange={(v) => onApiKeyFieldChange(v as ClaudeApiKeyField)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ANTHROPIC_AUTH_TOKEN">
|
||||
{t("providerForm.authFieldAuthToken", {
|
||||
defaultValue: "ANTHROPIC_AUTH_TOKEN(默认)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="ANTHROPIC_API_KEY">
|
||||
{t("providerForm.authFieldApiKey", {
|
||||
defaultValue: "ANTHROPIC_API_KEY",
|
||||
})}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("providerForm.authFieldHint", {
|
||||
defaultValue: "选择写入配置的认证环境变量名",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 模型选择器 */}
|
||||
{shouldShowModelSelector && (
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -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<ClaudeApiKeyField>(
|
||||
() => {
|
||||
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<string, unknown>)
|
||||
?.env as Record<string, unknown> | 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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 モデル",
|
||||
|
||||
@@ -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 默认模型",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user