From 5f78e58ffc1a4a8ad790eaa307dafa784cf27b3a Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 6 Nov 2025 15:22:38 +0800 Subject: [PATCH] feat: add partner promotion feature for Zhipu GLM - Add isPartner and partnerPromotionKey fields to Provider and ProviderPreset types - Display gold star badge on partner presets in selector - Show promotional message in API Key section for partners - Configure Zhipu GLM as official partner with 10% discount promotion - Support both Claude and Codex provider presets - Add i18n support for partner promotion messages (zh/en) --- .../providers/forms/ClaudeFormFields.tsx | 6 ++++ .../providers/forms/CodexFormFields.tsx | 6 ++++ .../providers/forms/ProviderForm.tsx | 15 ++++++++ .../forms/ProviderPresetSelector.tsx | 10 ++++-- .../providers/forms/hooks/useApiKeyLink.ts | 36 +++++++++++++------ .../providers/forms/shared/ApiKeySection.tsx | 18 +++++++++- src/config/claudeProviderPresets.ts | 5 +++ src/config/codexProviderPresets.ts | 2 ++ src/i18n/locales/en.json | 3 ++ src/i18n/locales/zh.json | 3 ++ src/types.ts | 2 ++ 11 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx index 252bace..4ba930b 100644 --- a/src/components/providers/forms/ClaudeFormFields.tsx +++ b/src/components/providers/forms/ClaudeFormFields.tsx @@ -19,6 +19,8 @@ interface ClaudeFormFieldsProps { category?: ProviderCategory; shouldShowApiKeyLink: boolean; websiteUrl: string; + isPartner?: boolean; + partnerPromotionKey?: string; // Template Values templateValueEntries: Array<[string, TemplateValueConfig]>; @@ -61,6 +63,8 @@ export function ClaudeFormFields({ category, shouldShowApiKeyLink, websiteUrl, + isPartner, + partnerPromotionKey, templateValueEntries, templateValues, templatePresetName, @@ -91,6 +95,8 @@ export function ClaudeFormFields({ category={category} shouldShowLink={shouldShowApiKeyLink} websiteUrl={websiteUrl} + isPartner={isPartner} + partnerPromotionKey={partnerPromotionKey} /> )} diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index 88a3537..686610d 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -15,6 +15,8 @@ interface CodexFormFieldsProps { category?: ProviderCategory; shouldShowApiKeyLink: boolean; websiteUrl: string; + isPartner?: boolean; + partnerPromotionKey?: string; // Base URL shouldShowSpeedTest: boolean; @@ -35,6 +37,8 @@ export function CodexFormFields({ category, shouldShowApiKeyLink, websiteUrl, + isPartner, + partnerPromotionKey, shouldShowSpeedTest, codexBaseUrl, onBaseUrlChange, @@ -56,6 +60,8 @@ export function CodexFormFields({ category={category} shouldShowLink={shouldShowApiKeyLink} websiteUrl={websiteUrl} + isPartner={isPartner} + partnerPromotionKey={partnerPromotionKey} placeholder={{ official: t("providerForm.codexOfficialNoApiKey", { defaultValue: "官方供应商无需 API Key", diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 6aae4f8..6d6d6ef 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -79,6 +79,7 @@ export function ProviderForm({ const [activePreset, setActivePreset] = useState<{ id: string; category?: ProviderCategory; + isPartner?: boolean; } | null>(null); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); @@ -326,6 +327,10 @@ export function ProviderForm({ if (activePreset.category) { payload.presetCategory = activePreset.category; } + // 继承合作伙伴标识 + if (activePreset.isPartner) { + payload.isPartner = activePreset.isPartner; + } } // 处理 meta 字段:基于 draftCustomEndpoints 生成 custom_endpoints @@ -399,6 +404,8 @@ export function ProviderForm({ const { shouldShowApiKeyLink: shouldShowClaudeApiKeyLink, websiteUrl: claudeWebsiteUrl, + isPartner: isClaudePartner, + partnerPromotionKey: claudePartnerPromotionKey, } = useApiKeyLink({ appId: "claude", category, @@ -411,6 +418,8 @@ export function ProviderForm({ const { shouldShowApiKeyLink: shouldShowCodexApiKeyLink, websiteUrl: codexWebsiteUrl, + isPartner: isCodexPartner, + partnerPromotionKey: codexPartnerPromotionKey, } = useApiKeyLink({ appId: "codex", category, @@ -450,6 +459,7 @@ export function ProviderForm({ setActivePreset({ id: value, category: entry.preset.category, + isPartner: entry.preset.isPartner, }); if (appId === "codex") { @@ -523,6 +533,8 @@ export function ProviderForm({ category={category} shouldShowApiKeyLink={shouldShowClaudeApiKeyLink} websiteUrl={claudeWebsiteUrl} + isPartner={isClaudePartner} + partnerPromotionKey={claudePartnerPromotionKey} templateValueEntries={templateValueEntries} templateValues={templateValues} templatePresetName={templatePreset?.name || ""} @@ -552,6 +564,8 @@ export function ProviderForm({ category={category} shouldShowApiKeyLink={shouldShowCodexApiKeyLink} websiteUrl={codexWebsiteUrl} + isPartner={isCodexPartner} + partnerPromotionKey={codexPartnerPromotionKey} shouldShowSpeedTest={shouldShowSpeedTest} codexBaseUrl={codexBaseUrl} onBaseUrlChange={handleCodexBaseUrlChange} @@ -612,5 +626,6 @@ export function ProviderForm({ export type ProviderFormValues = ProviderFormData & { presetId?: string; presetCategory?: ProviderCategory; + isPartner?: boolean; meta?: ProviderMeta; }; diff --git a/src/components/providers/forms/ProviderPresetSelector.tsx b/src/components/providers/forms/ProviderPresetSelector.tsx index c318000..20a301b 100644 --- a/src/components/providers/forms/ProviderPresetSelector.tsx +++ b/src/components/providers/forms/ProviderPresetSelector.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { FormLabel } from "@/components/ui/form"; import { ClaudeIcon, CodexIcon } from "@/components/BrandIcons"; -import { Zap } from "lucide-react"; +import { Zap, Star } from "lucide-react"; import type { ProviderPreset } from "@/config/claudeProviderPresets"; import type { CodexProviderPreset } from "@/config/codexProviderPresets"; import type { ProviderCategory } from "@/types"; @@ -157,12 +157,13 @@ export function ProviderPresetSelector({ if (!entries || entries.length === 0) return null; return entries.map((entry) => { const isSelected = selectedPresetId === entry.id; + const isPartner = entry.preset.isPartner; return ( ); }); diff --git a/src/components/providers/forms/hooks/useApiKeyLink.ts b/src/components/providers/forms/hooks/useApiKeyLink.ts index 95080df..f25c9b6 100644 --- a/src/components/providers/forms/hooks/useApiKeyLink.ts +++ b/src/components/providers/forms/hooks/useApiKeyLink.ts @@ -37,20 +37,34 @@ export function useApiKeyLink({ ); }, [category]); + // 获取当前预设条目 + const currentPresetEntry = useMemo(() => { + if (selectedPresetId && selectedPresetId !== "custom") { + return presetEntries.find((item) => item.id === selectedPresetId); + } + return undefined; + }, [selectedPresetId, presetEntries]); + // 获取当前供应商的网址(用于 API Key 链接) const getWebsiteUrl = useMemo(() => { - if (selectedPresetId && selectedPresetId !== "custom") { - const entry = presetEntries.find((item) => item.id === selectedPresetId); - if (entry) { - const preset = entry.preset; - // 第三方供应商优先使用 apiKeyUrl - return preset.category === "third_party" - ? preset.apiKeyUrl || preset.websiteUrl || "" - : preset.websiteUrl || ""; - } + if (currentPresetEntry) { + const preset = currentPresetEntry.preset; + // 第三方供应商优先使用 apiKeyUrl + return preset.category === "third_party" + ? preset.apiKeyUrl || preset.websiteUrl || "" + : preset.websiteUrl || ""; } return formWebsiteUrl || ""; - }, [selectedPresetId, presetEntries, formWebsiteUrl]); + }, [currentPresetEntry, formWebsiteUrl]); + + // 提取合作伙伴信息 + const isPartner = useMemo(() => { + return currentPresetEntry?.preset.isPartner ?? false; + }, [currentPresetEntry]); + + const partnerPromotionKey = useMemo(() => { + return currentPresetEntry?.preset.partnerPromotionKey; + }, [currentPresetEntry]); return { shouldShowApiKeyLink: @@ -60,5 +74,7 @@ export function useApiKeyLink({ ? shouldShowApiKeyLink : false, websiteUrl: getWebsiteUrl, + isPartner, + partnerPromotionKey, }; } diff --git a/src/components/providers/forms/shared/ApiKeySection.tsx b/src/components/providers/forms/shared/ApiKeySection.tsx index 049dce0..b873eb0 100644 --- a/src/components/providers/forms/shared/ApiKeySection.tsx +++ b/src/components/providers/forms/shared/ApiKeySection.tsx @@ -15,6 +15,8 @@ interface ApiKeySectionProps { thirdParty: string; }; disabled?: boolean; + isPartner?: boolean; + partnerPromotionKey?: string; } export function ApiKeySection({ @@ -27,6 +29,8 @@ export function ApiKeySection({ websiteUrl, placeholder, disabled, + isPartner, + partnerPromotionKey, }: ApiKeySectionProps) { const { t } = useTranslation(); @@ -57,7 +61,7 @@ export function ApiKeySection({ /> {/* API Key 获取链接 */} {shouldShowLink && websiteUrl && ( -
+
+ + {/* 合作伙伴促销信息 */} + {isPartner && partnerPromotionKey && ( +
+

+ 💡{" "} + {t(`providerForm.partnerPromotion.${partnerPromotionKey}`, { + defaultValue: "", + })} +

+
+ )}
)}
diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index a586712..f4b35e5 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -29,6 +29,8 @@ export interface ProviderPreset { apiKeyUrl?: string; settingsConfig: object; isOfficial?: boolean; // 标识是否为官方预设 + isPartner?: boolean; // 标识是否为商业合作伙伴 + partnerPromotionKey?: string; // 合作伙伴促销信息的 i18n key category?: ProviderCategory; // 新增:分类 // 新增:指定该预设所使用的 API Key 字段名(默认 ANTHROPIC_AUTH_TOKEN) apiKeyField?: "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY"; @@ -73,6 +75,7 @@ export const providerPresets: ProviderPreset[] = [ { name: "Zhipu GLM", websiteUrl: "https://open.bigmodel.cn", + apiKeyUrl: "https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII", settingsConfig: { env: { ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic", @@ -84,6 +87,8 @@ export const providerPresets: ProviderPreset[] = [ }, }, category: "cn_official", + isPartner: true, // 商业合作伙伴 + partnerPromotionKey: "zhipu", // 促销信息 i18n key }, { name: "Qwen Coder", diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index 83c1312..643d7c5 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -12,6 +12,8 @@ export interface CodexProviderPreset { auth: Record; // 将写入 ~/.codex/auth.json config: string; // 将写入 ~/.codex/config.toml(TOML 字符串) isOfficial?: boolean; // 标识是否为官方预设 + isPartner?: boolean; // 标识是否为商业合作伙伴 + partnerPromotionKey?: string; // 合作伙伴促销信息的 i18n key category?: ProviderCategory; // 新增:分类 isCustomTemplate?: boolean; // 标识是否为自定义模板 // 新增:请求地址候选列表(用于地址管理/测速) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b2b9439..17c6b6d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -238,6 +238,9 @@ "customApiKeyHint": "💡 Custom configuration requires manually filling all necessary fields", "officialHint": "💡 Official provider uses browser login, no API Key needed", "getApiKey": "Get API Key", + "partnerPromotion": { + "zhipu": "Zhipu GLM is an official partner of CC Switch. Use this link to top up and get a 10% discount" + }, "parameterConfig": "Parameter Config - {{name}} *", "mainModel": "Main Model (optional)", "mainModelPlaceholder": "e.g., GLM-4.6", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 0174a57..a5a8b04 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -238,6 +238,9 @@ "customApiKeyHint": "💡 自定义配置需手动填写所有必要字段", "officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key", "getApiKey": "获取 API Key", + "partnerPromotion": { + "zhipu": "智谱 GLM 是 CC Switch 的官方合作伙伴,使用此链接充值可以获得9折优惠" + }, "parameterConfig": "参数配置 - {{name}} *", "mainModel": "主模型 (可选)", "mainModelPlaceholder": "例如: GLM-4.6", diff --git a/src/types.ts b/src/types.ts index 07f2e6d..ae44b29 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,8 @@ export interface Provider { category?: ProviderCategory; createdAt?: number; // 添加时间戳(毫秒) sortIndex?: number; // 排序索引(用于自定义拖拽排序) + // 新增:是否为商业合作伙伴 + isPartner?: boolean; // 可选:供应商元数据(仅存于 ~/.cc-switch/config.json,不写入 live 配置) meta?: ProviderMeta; }