mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 18:32:43 +08:00
feat(provider): add auth field selector for Claude providers (AUTH_TOKEN / API_KEY)
Allow users to choose between ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY when creating or editing custom Claude providers, persisted in meta.apiKeyField.
This commit is contained in:
@@ -235,6 +235,11 @@ pub struct ProviderMeta {
|
|||||||
/// - "openai_chat": OpenAI Chat Completions 格式,需要转换
|
/// - "openai_chat": OpenAI Chat Completions 格式,需要转换
|
||||||
#[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")]
|
||||||
pub api_format: Option<String>,
|
pub api_format: Option<String>,
|
||||||
|
/// Claude 认证字段名(仅 Claude 供应商使用)
|
||||||
|
/// - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商
|
||||||
|
/// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key
|
||||||
|
#[serde(rename = "apiKeyField", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub api_key_field: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProviderManager {
|
impl ProviderManager {
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import EndpointSpeedTest from "./EndpointSpeedTest";
|
import EndpointSpeedTest from "./EndpointSpeedTest";
|
||||||
import { ApiKeySection, EndpointField } from "./shared";
|
import { ApiKeySection, EndpointField } from "./shared";
|
||||||
import type { ProviderCategory, ClaudeApiFormat } from "@/types";
|
import type {
|
||||||
|
ProviderCategory,
|
||||||
|
ClaudeApiFormat,
|
||||||
|
ClaudeApiKeyField,
|
||||||
|
} from "@/types";
|
||||||
import type { TemplateValueConfig } from "@/config/claudeProviderPresets";
|
import type { TemplateValueConfig } from "@/config/claudeProviderPresets";
|
||||||
|
|
||||||
interface EndpointCandidate {
|
interface EndpointCandidate {
|
||||||
@@ -68,6 +72,10 @@ interface ClaudeFormFieldsProps {
|
|||||||
// API Format (for third-party providers that use OpenAI Chat Completions format)
|
// API Format (for third-party providers that use OpenAI Chat Completions format)
|
||||||
apiFormat: ClaudeApiFormat;
|
apiFormat: ClaudeApiFormat;
|
||||||
onApiFormatChange: (format: ClaudeApiFormat) => void;
|
onApiFormatChange: (format: ClaudeApiFormat) => void;
|
||||||
|
|
||||||
|
// Auth Key Field (ANTHROPIC_AUTH_TOKEN vs ANTHROPIC_API_KEY)
|
||||||
|
apiKeyField: ClaudeApiKeyField;
|
||||||
|
onApiKeyFieldChange: (field: ClaudeApiKeyField) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClaudeFormFields({
|
export function ClaudeFormFields({
|
||||||
@@ -102,6 +110,8 @@ export function ClaudeFormFields({
|
|||||||
speedTestEndpoints,
|
speedTestEndpoints,
|
||||||
apiFormat,
|
apiFormat,
|
||||||
onApiFormatChange,
|
onApiFormatChange,
|
||||||
|
apiKeyField,
|
||||||
|
onApiKeyFieldChange,
|
||||||
}: ClaudeFormFieldsProps) {
|
}: ClaudeFormFieldsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -219,6 +229,41 @@ export function ClaudeFormFields({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 认证字段选择(仅非官方供应商显示) */}
|
||||||
|
{shouldShowModelSelector && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel htmlFor="apiKeyField">
|
||||||
|
{t("providerForm.authField", { defaultValue: "认证字段" })}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={apiKeyField}
|
||||||
|
onValueChange={(v) => onApiKeyFieldChange(v as ClaudeApiKeyField)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="apiKeyField" className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ANTHROPIC_AUTH_TOKEN">
|
||||||
|
{t("providerForm.authFieldAuthToken", {
|
||||||
|
defaultValue: "Auth Token (默认)",
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="ANTHROPIC_API_KEY">
|
||||||
|
{t("providerForm.authFieldApiKey", {
|
||||||
|
defaultValue: "API Key",
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("providerForm.authFieldHint", {
|
||||||
|
defaultValue:
|
||||||
|
"大多数第三方供应商使用 Auth Token;少数供应商需要 API Key",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 模型选择器 */}
|
{/* 模型选择器 */}
|
||||||
{shouldShowModelSelector && (
|
{shouldShowModelSelector && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
ProviderTestConfig,
|
ProviderTestConfig,
|
||||||
ProviderProxyConfig,
|
ProviderProxyConfig,
|
||||||
ClaudeApiFormat,
|
ClaudeApiFormat,
|
||||||
|
ClaudeApiKeyField,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import {
|
import {
|
||||||
providerPresets,
|
providerPresets,
|
||||||
@@ -240,6 +241,55 @@ export function ProviderForm({
|
|||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
|
||||||
|
if (appId !== "claude") return "anthropic";
|
||||||
|
return initialData?.meta?.apiFormat ?? "anthropic";
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => {
|
||||||
|
setLocalApiFormat(format);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [localApiKeyField, setLocalApiKeyField] = useState<ClaudeApiKeyField>(
|
||||||
|
() => {
|
||||||
|
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<string, unknown>).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<string, unknown>;
|
||||||
|
};
|
||||||
|
const env = (config.env ?? {}) as Record<string, unknown>;
|
||||||
|
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 {
|
const {
|
||||||
apiKey,
|
apiKey,
|
||||||
handleApiKeyChange,
|
handleApiKeyChange,
|
||||||
@@ -250,6 +300,7 @@ export function ProviderForm({
|
|||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
category,
|
category,
|
||||||
appType: appId,
|
appType: appId,
|
||||||
|
apiKeyField: appId === "claude" ? localApiKeyField : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({
|
const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({
|
||||||
@@ -273,15 +324,6 @@ export function ProviderForm({
|
|||||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
|
|
||||||
if (appId !== "claude") return "anthropic";
|
|
||||||
return initialData?.meta?.apiFormat ?? "anthropic";
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => {
|
|
||||||
setLocalApiFormat(format);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
codexAuth,
|
codexAuth,
|
||||||
codexConfig,
|
codexConfig,
|
||||||
@@ -775,6 +817,10 @@ export function ProviderForm({
|
|||||||
appId === "claude" && category !== "official"
|
appId === "claude" && category !== "official"
|
||||||
? localApiFormat
|
? localApiFormat
|
||||||
: undefined,
|
: undefined,
|
||||||
|
apiKeyField:
|
||||||
|
appId === "claude" && category !== "official"
|
||||||
|
? localApiKeyField
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit(payload);
|
onSubmit(payload);
|
||||||
@@ -1012,6 +1058,12 @@ export function ProviderForm({
|
|||||||
setLocalApiFormat("anthropic");
|
setLocalApiFormat("anthropic");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preset.apiKeyField) {
|
||||||
|
setLocalApiKeyField(preset.apiKeyField);
|
||||||
|
} else {
|
||||||
|
setLocalApiKeyField("ANTHROPIC_AUTH_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
form.reset({
|
form.reset({
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
websiteUrl: preset.websiteUrl ?? "",
|
websiteUrl: preset.websiteUrl ?? "",
|
||||||
@@ -1216,6 +1268,8 @@ export function ProviderForm({
|
|||||||
speedTestEndpoints={speedTestEndpoints}
|
speedTestEndpoints={speedTestEndpoints}
|
||||||
apiFormat={localApiFormat}
|
apiFormat={localApiFormat}
|
||||||
onApiFormatChange={handleApiFormatChange}
|
onApiFormatChange={handleApiFormatChange}
|
||||||
|
apiKeyField={localApiKeyField}
|
||||||
|
onApiKeyFieldChange={handleApiKeyFieldChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface UseApiKeyStateProps {
|
|||||||
selectedPresetId: string | null;
|
selectedPresetId: string | null;
|
||||||
category?: ProviderCategory;
|
category?: ProviderCategory;
|
||||||
appType?: string;
|
appType?: string;
|
||||||
|
apiKeyField?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +25,7 @@ export function useApiKeyState({
|
|||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
category,
|
category,
|
||||||
appType,
|
appType,
|
||||||
|
apiKeyField,
|
||||||
}: UseApiKeyStateProps) {
|
}: UseApiKeyStateProps) {
|
||||||
const [apiKey, setApiKey] = useState(() => {
|
const [apiKey, setApiKey] = useState(() => {
|
||||||
if (initialConfig) {
|
if (initialConfig) {
|
||||||
@@ -58,7 +60,7 @@ export function useApiKeyState({
|
|||||||
initialConfig || "{}",
|
initialConfig || "{}",
|
||||||
key.trim(),
|
key.trim(),
|
||||||
{
|
{
|
||||||
// 最佳实践:仅在“新增模式”且“非官方类别”时补齐缺失字段
|
// 最佳实践:仅在"新增模式"且"非官方类别"时补齐缺失字段
|
||||||
// - 新增模式:selectedPresetId !== null
|
// - 新增模式:selectedPresetId !== null
|
||||||
// - 非官方类别:category !== undefined && category !== "official"
|
// - 非官方类别:category !== undefined && category !== "official"
|
||||||
// - 官方类别:不创建字段(UI 也会禁用输入框)
|
// - 官方类别:不创建字段(UI 也会禁用输入框)
|
||||||
@@ -68,12 +70,20 @@ export function useApiKeyState({
|
|||||||
category !== undefined &&
|
category !== undefined &&
|
||||||
category !== "official",
|
category !== "official",
|
||||||
appType,
|
appType,
|
||||||
|
apiKeyField,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
onConfigChange(configString);
|
onConfigChange(configString);
|
||||||
},
|
},
|
||||||
[initialConfig, selectedPresetId, category, appType, onConfigChange],
|
[
|
||||||
|
initialConfig,
|
||||||
|
selectedPresetId,
|
||||||
|
category,
|
||||||
|
appType,
|
||||||
|
apiKeyField,
|
||||||
|
onConfigChange,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const showApiKey = useCallback(
|
const showApiKey = useCallback(
|
||||||
|
|||||||
@@ -655,6 +655,10 @@
|
|||||||
"apiFormatHint": "Select the input format for the provider's API",
|
"apiFormatHint": "Select the input format for the provider's API",
|
||||||
"apiFormatAnthropic": "Anthropic Messages (Native)",
|
"apiFormatAnthropic": "Anthropic Messages (Native)",
|
||||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)",
|
"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",
|
"anthropicDefaultHaikuModel": "Default Haiku Model",
|
||||||
"anthropicDefaultSonnetModel": "Default Sonnet Model",
|
"anthropicDefaultSonnetModel": "Default Sonnet Model",
|
||||||
"anthropicDefaultOpusModel": "Default Opus Model",
|
"anthropicDefaultOpusModel": "Default Opus Model",
|
||||||
|
|||||||
@@ -655,6 +655,10 @@
|
|||||||
"apiFormatHint": "プロバイダー API の入力フォーマットを選択",
|
"apiFormatHint": "プロバイダー API の入力フォーマットを選択",
|
||||||
"apiFormatAnthropic": "Anthropic Messages(ネイティブ)",
|
"apiFormatAnthropic": "Anthropic Messages(ネイティブ)",
|
||||||
"apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)",
|
"apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)",
|
||||||
|
"authField": "認証フィールド",
|
||||||
|
"authFieldAuthToken": "Auth Token(デフォルト)",
|
||||||
|
"authFieldApiKey": "API Key",
|
||||||
|
"authFieldHint": "ほとんどのサードパーティプロバイダーは Auth Token を使用します。一部は API Key が必要です",
|
||||||
"anthropicDefaultHaikuModel": "既定 Haiku モデル",
|
"anthropicDefaultHaikuModel": "既定 Haiku モデル",
|
||||||
"anthropicDefaultSonnetModel": "既定 Sonnet モデル",
|
"anthropicDefaultSonnetModel": "既定 Sonnet モデル",
|
||||||
"anthropicDefaultOpusModel": "既定 Opus モデル",
|
"anthropicDefaultOpusModel": "既定 Opus モデル",
|
||||||
|
|||||||
@@ -655,6 +655,10 @@
|
|||||||
"apiFormatHint": "选择供应商 API 的输入格式",
|
"apiFormatHint": "选择供应商 API 的输入格式",
|
||||||
"apiFormatAnthropic": "Anthropic Messages (原生)",
|
"apiFormatAnthropic": "Anthropic Messages (原生)",
|
||||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)",
|
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)",
|
||||||
|
"authField": "认证字段",
|
||||||
|
"authFieldAuthToken": "Auth Token (默认)",
|
||||||
|
"authFieldApiKey": "API Key",
|
||||||
|
"authFieldHint": "大多数第三方供应商使用 Auth Token;少数供应商需要 API Key",
|
||||||
"anthropicDefaultHaikuModel": "Haiku 默认模型",
|
"anthropicDefaultHaikuModel": "Haiku 默认模型",
|
||||||
"anthropicDefaultSonnetModel": "Sonnet 默认模型",
|
"anthropicDefaultSonnetModel": "Sonnet 默认模型",
|
||||||
"anthropicDefaultOpusModel": "Opus 默认模型",
|
"anthropicDefaultOpusModel": "Opus 默认模型",
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ export interface ProviderMeta {
|
|||||||
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
||||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||||
apiFormat?: "anthropic" | "openai_chat";
|
apiFormat?: "anthropic" | "openai_chat";
|
||||||
|
// Claude 认证字段名(仅 Claude 供应商使用)
|
||||||
|
// - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商
|
||||||
|
// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key
|
||||||
|
apiKeyField?: "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skill 同步方式
|
// Skill 同步方式
|
||||||
@@ -155,6 +159,11 @@ export type SkillSyncMethod = "auto" | "symlink" | "copy";
|
|||||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||||
export type ClaudeApiFormat = "anthropic" | "openai_chat";
|
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 {
|
export interface VisibleApps {
|
||||||
claude: boolean;
|
claude: boolean;
|
||||||
|
|||||||
@@ -135,9 +135,13 @@ export const hasApiKeyField = (
|
|||||||
export const setApiKeyInConfig = (
|
export const setApiKeyInConfig = (
|
||||||
jsonString: string,
|
jsonString: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
options: { createIfMissing?: boolean; appType?: string } = {},
|
options: {
|
||||||
|
createIfMissing?: boolean;
|
||||||
|
appType?: string;
|
||||||
|
apiKeyField?: string;
|
||||||
|
} = {},
|
||||||
): string => {
|
): string => {
|
||||||
const { createIfMissing = false, appType } = options;
|
const { createIfMissing = false, appType, apiKeyField } = options;
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(jsonString);
|
const config = JSON.parse(jsonString);
|
||||||
if (!config.env) {
|
if (!config.env) {
|
||||||
@@ -170,13 +174,13 @@ export const setApiKeyInConfig = (
|
|||||||
return JSON.stringify(config, null, 2);
|
return JSON.stringify(config, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则默认创建 AUTH_TOKEN 字段)
|
// Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则使用 apiKeyField 指定的字段名)
|
||||||
if ("ANTHROPIC_AUTH_TOKEN" in env) {
|
if ("ANTHROPIC_AUTH_TOKEN" in env) {
|
||||||
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
||||||
} else if ("ANTHROPIC_API_KEY" in env) {
|
} else if ("ANTHROPIC_API_KEY" in env) {
|
||||||
env.ANTHROPIC_API_KEY = apiKey;
|
env.ANTHROPIC_API_KEY = apiKey;
|
||||||
} else if (createIfMissing) {
|
} else if (createIfMissing) {
|
||||||
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
env[apiKeyField ?? "ANTHROPIC_AUTH_TOKEN"] = apiKey;
|
||||||
} else {
|
} else {
|
||||||
return jsonString;
|
return jsonString;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user