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)
This commit is contained in:
Jason
2025-11-06 15:22:38 +08:00
parent e4416c9da8
commit 5f78e58ffc
11 changed files with 93 additions and 13 deletions

View File

@@ -19,6 +19,8 @@ interface ClaudeFormFieldsProps {
category?: ProviderCategory; category?: ProviderCategory;
shouldShowApiKeyLink: boolean; shouldShowApiKeyLink: boolean;
websiteUrl: string; websiteUrl: string;
isPartner?: boolean;
partnerPromotionKey?: string;
// Template Values // Template Values
templateValueEntries: Array<[string, TemplateValueConfig]>; templateValueEntries: Array<[string, TemplateValueConfig]>;
@@ -61,6 +63,8 @@ export function ClaudeFormFields({
category, category,
shouldShowApiKeyLink, shouldShowApiKeyLink,
websiteUrl, websiteUrl,
isPartner,
partnerPromotionKey,
templateValueEntries, templateValueEntries,
templateValues, templateValues,
templatePresetName, templatePresetName,
@@ -91,6 +95,8 @@ export function ClaudeFormFields({
category={category} category={category}
shouldShowLink={shouldShowApiKeyLink} shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl} websiteUrl={websiteUrl}
isPartner={isPartner}
partnerPromotionKey={partnerPromotionKey}
/> />
)} )}

View File

@@ -15,6 +15,8 @@ interface CodexFormFieldsProps {
category?: ProviderCategory; category?: ProviderCategory;
shouldShowApiKeyLink: boolean; shouldShowApiKeyLink: boolean;
websiteUrl: string; websiteUrl: string;
isPartner?: boolean;
partnerPromotionKey?: string;
// Base URL // Base URL
shouldShowSpeedTest: boolean; shouldShowSpeedTest: boolean;
@@ -35,6 +37,8 @@ export function CodexFormFields({
category, category,
shouldShowApiKeyLink, shouldShowApiKeyLink,
websiteUrl, websiteUrl,
isPartner,
partnerPromotionKey,
shouldShowSpeedTest, shouldShowSpeedTest,
codexBaseUrl, codexBaseUrl,
onBaseUrlChange, onBaseUrlChange,
@@ -56,6 +60,8 @@ export function CodexFormFields({
category={category} category={category}
shouldShowLink={shouldShowApiKeyLink} shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl} websiteUrl={websiteUrl}
isPartner={isPartner}
partnerPromotionKey={partnerPromotionKey}
placeholder={{ placeholder={{
official: t("providerForm.codexOfficialNoApiKey", { official: t("providerForm.codexOfficialNoApiKey", {
defaultValue: "官方供应商无需 API Key", defaultValue: "官方供应商无需 API Key",

View File

@@ -79,6 +79,7 @@ export function ProviderForm({
const [activePreset, setActivePreset] = useState<{ const [activePreset, setActivePreset] = useState<{
id: string; id: string;
category?: ProviderCategory; category?: ProviderCategory;
isPartner?: boolean;
} | null>(null); } | null>(null);
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
@@ -326,6 +327,10 @@ export function ProviderForm({
if (activePreset.category) { if (activePreset.category) {
payload.presetCategory = activePreset.category; payload.presetCategory = activePreset.category;
} }
// 继承合作伙伴标识
if (activePreset.isPartner) {
payload.isPartner = activePreset.isPartner;
}
} }
// 处理 meta 字段:基于 draftCustomEndpoints 生成 custom_endpoints // 处理 meta 字段:基于 draftCustomEndpoints 生成 custom_endpoints
@@ -399,6 +404,8 @@ export function ProviderForm({
const { const {
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink, shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
websiteUrl: claudeWebsiteUrl, websiteUrl: claudeWebsiteUrl,
isPartner: isClaudePartner,
partnerPromotionKey: claudePartnerPromotionKey,
} = useApiKeyLink({ } = useApiKeyLink({
appId: "claude", appId: "claude",
category, category,
@@ -411,6 +418,8 @@ export function ProviderForm({
const { const {
shouldShowApiKeyLink: shouldShowCodexApiKeyLink, shouldShowApiKeyLink: shouldShowCodexApiKeyLink,
websiteUrl: codexWebsiteUrl, websiteUrl: codexWebsiteUrl,
isPartner: isCodexPartner,
partnerPromotionKey: codexPartnerPromotionKey,
} = useApiKeyLink({ } = useApiKeyLink({
appId: "codex", appId: "codex",
category, category,
@@ -450,6 +459,7 @@ export function ProviderForm({
setActivePreset({ setActivePreset({
id: value, id: value,
category: entry.preset.category, category: entry.preset.category,
isPartner: entry.preset.isPartner,
}); });
if (appId === "codex") { if (appId === "codex") {
@@ -523,6 +533,8 @@ export function ProviderForm({
category={category} category={category}
shouldShowApiKeyLink={shouldShowClaudeApiKeyLink} shouldShowApiKeyLink={shouldShowClaudeApiKeyLink}
websiteUrl={claudeWebsiteUrl} websiteUrl={claudeWebsiteUrl}
isPartner={isClaudePartner}
partnerPromotionKey={claudePartnerPromotionKey}
templateValueEntries={templateValueEntries} templateValueEntries={templateValueEntries}
templateValues={templateValues} templateValues={templateValues}
templatePresetName={templatePreset?.name || ""} templatePresetName={templatePreset?.name || ""}
@@ -552,6 +564,8 @@ export function ProviderForm({
category={category} category={category}
shouldShowApiKeyLink={shouldShowCodexApiKeyLink} shouldShowApiKeyLink={shouldShowCodexApiKeyLink}
websiteUrl={codexWebsiteUrl} websiteUrl={codexWebsiteUrl}
isPartner={isCodexPartner}
partnerPromotionKey={codexPartnerPromotionKey}
shouldShowSpeedTest={shouldShowSpeedTest} shouldShowSpeedTest={shouldShowSpeedTest}
codexBaseUrl={codexBaseUrl} codexBaseUrl={codexBaseUrl}
onBaseUrlChange={handleCodexBaseUrlChange} onBaseUrlChange={handleCodexBaseUrlChange}
@@ -612,5 +626,6 @@ export function ProviderForm({
export type ProviderFormValues = ProviderFormData & { export type ProviderFormValues = ProviderFormData & {
presetId?: string; presetId?: string;
presetCategory?: ProviderCategory; presetCategory?: ProviderCategory;
isPartner?: boolean;
meta?: ProviderMeta; meta?: ProviderMeta;
}; };

View File

@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form"; import { FormLabel } from "@/components/ui/form";
import { ClaudeIcon, CodexIcon } from "@/components/BrandIcons"; 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 { ProviderPreset } from "@/config/claudeProviderPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets"; import type { CodexProviderPreset } from "@/config/codexProviderPresets";
import type { ProviderCategory } from "@/types"; import type { ProviderCategory } from "@/types";
@@ -157,12 +157,13 @@ export function ProviderPresetSelector({
if (!entries || entries.length === 0) return null; if (!entries || entries.length === 0) return null;
return entries.map((entry) => { return entries.map((entry) => {
const isSelected = selectedPresetId === entry.id; const isSelected = selectedPresetId === entry.id;
const isPartner = entry.preset.isPartner;
return ( return (
<button <button
key={entry.id} key={entry.id}
type="button" type="button"
onClick={() => onPresetChange(entry.id)} onClick={() => onPresetChange(entry.id)}
className={getPresetButtonClass(isSelected, entry.preset)} className={`${getPresetButtonClass(isSelected, entry.preset)} relative`}
style={getPresetButtonStyle(isSelected, entry.preset)} style={getPresetButtonStyle(isSelected, entry.preset)}
title={ title={
presetCategoryLabels[category] ?? presetCategoryLabels[category] ??
@@ -173,6 +174,11 @@ export function ProviderPresetSelector({
> >
{renderPresetIcon(entry.preset)} {renderPresetIcon(entry.preset)}
{entry.preset.name} {entry.preset.name}
{isPartner && (
<span className="absolute -top-1 -right-1 flex items-center gap-0.5 rounded-full bg-gradient-to-r from-amber-500 to-yellow-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-md">
<Star className="h-2.5 w-2.5 fill-current" />
</span>
)}
</button> </button>
); );
}); });

View File

@@ -37,20 +37,34 @@ export function useApiKeyLink({
); );
}, [category]); }, [category]);
// 获取当前预设条目
const currentPresetEntry = useMemo(() => {
if (selectedPresetId && selectedPresetId !== "custom") {
return presetEntries.find((item) => item.id === selectedPresetId);
}
return undefined;
}, [selectedPresetId, presetEntries]);
// 获取当前供应商的网址(用于 API Key 链接) // 获取当前供应商的网址(用于 API Key 链接)
const getWebsiteUrl = useMemo(() => { const getWebsiteUrl = useMemo(() => {
if (selectedPresetId && selectedPresetId !== "custom") { if (currentPresetEntry) {
const entry = presetEntries.find((item) => item.id === selectedPresetId); const preset = currentPresetEntry.preset;
if (entry) { // 第三方供应商优先使用 apiKeyUrl
const preset = entry.preset; return preset.category === "third_party"
// 第三方供应商优先使用 apiKeyUrl ? preset.apiKeyUrl || preset.websiteUrl || ""
return preset.category === "third_party" : preset.websiteUrl || "";
? preset.apiKeyUrl || preset.websiteUrl || ""
: preset.websiteUrl || "";
}
} }
return formWebsiteUrl || ""; 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 { return {
shouldShowApiKeyLink: shouldShowApiKeyLink:
@@ -60,5 +74,7 @@ export function useApiKeyLink({
? shouldShowApiKeyLink ? shouldShowApiKeyLink
: false, : false,
websiteUrl: getWebsiteUrl, websiteUrl: getWebsiteUrl,
isPartner,
partnerPromotionKey,
}; };
} }

View File

@@ -15,6 +15,8 @@ interface ApiKeySectionProps {
thirdParty: string; thirdParty: string;
}; };
disabled?: boolean; disabled?: boolean;
isPartner?: boolean;
partnerPromotionKey?: string;
} }
export function ApiKeySection({ export function ApiKeySection({
@@ -27,6 +29,8 @@ export function ApiKeySection({
websiteUrl, websiteUrl,
placeholder, placeholder,
disabled, disabled,
isPartner,
partnerPromotionKey,
}: ApiKeySectionProps) { }: ApiKeySectionProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -57,7 +61,7 @@ export function ApiKeySection({
/> />
{/* API Key 获取链接 */} {/* API Key 获取链接 */}
{shouldShowLink && websiteUrl && ( {shouldShowLink && websiteUrl && (
<div className="-mt-1 pl-1"> <div className="space-y-2 -mt-1 pl-1">
<a <a
href={websiteUrl} href={websiteUrl}
target="_blank" target="_blank"
@@ -68,6 +72,18 @@ export function ApiKeySection({
defaultValue: "获取 API Key", defaultValue: "获取 API Key",
})} })}
</a> </a>
{/* 合作伙伴促销信息 */}
{isPartner && partnerPromotionKey && (
<div className="rounded-md bg-blue-50 dark:bg-blue-950/30 p-2.5 border border-blue-200 dark:border-blue-800">
<p className="text-xs leading-relaxed text-blue-700 dark:text-blue-300">
💡{" "}
{t(`providerForm.partnerPromotion.${partnerPromotionKey}`, {
defaultValue: "",
})}
</p>
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -29,6 +29,8 @@ export interface ProviderPreset {
apiKeyUrl?: string; apiKeyUrl?: string;
settingsConfig: object; settingsConfig: object;
isOfficial?: boolean; // 标识是否为官方预设 isOfficial?: boolean; // 标识是否为官方预设
isPartner?: boolean; // 标识是否为商业合作伙伴
partnerPromotionKey?: string; // 合作伙伴促销信息的 i18n key
category?: ProviderCategory; // 新增:分类 category?: ProviderCategory; // 新增:分类
// 新增:指定该预设所使用的 API Key 字段名(默认 ANTHROPIC_AUTH_TOKEN // 新增:指定该预设所使用的 API Key 字段名(默认 ANTHROPIC_AUTH_TOKEN
apiKeyField?: "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY"; apiKeyField?: "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY";
@@ -73,6 +75,7 @@ export const providerPresets: ProviderPreset[] = [
{ {
name: "Zhipu GLM", name: "Zhipu GLM",
websiteUrl: "https://open.bigmodel.cn", websiteUrl: "https://open.bigmodel.cn",
apiKeyUrl: "https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII",
settingsConfig: { settingsConfig: {
env: { env: {
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic", ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
@@ -84,6 +87,8 @@ export const providerPresets: ProviderPreset[] = [
}, },
}, },
category: "cn_official", category: "cn_official",
isPartner: true, // 商业合作伙伴
partnerPromotionKey: "zhipu", // 促销信息 i18n key
}, },
{ {
name: "Qwen Coder", name: "Qwen Coder",

View File

@@ -12,6 +12,8 @@ export interface CodexProviderPreset {
auth: Record<string, any>; // 将写入 ~/.codex/auth.json auth: Record<string, any>; // 将写入 ~/.codex/auth.json
config: string; // 将写入 ~/.codex/config.tomlTOML 字符串) config: string; // 将写入 ~/.codex/config.tomlTOML 字符串)
isOfficial?: boolean; // 标识是否为官方预设 isOfficial?: boolean; // 标识是否为官方预设
isPartner?: boolean; // 标识是否为商业合作伙伴
partnerPromotionKey?: string; // 合作伙伴促销信息的 i18n key
category?: ProviderCategory; // 新增:分类 category?: ProviderCategory; // 新增:分类
isCustomTemplate?: boolean; // 标识是否为自定义模板 isCustomTemplate?: boolean; // 标识是否为自定义模板
// 新增:请求地址候选列表(用于地址管理/测速) // 新增:请求地址候选列表(用于地址管理/测速)

View File

@@ -238,6 +238,9 @@
"customApiKeyHint": "💡 Custom configuration requires manually filling all necessary fields", "customApiKeyHint": "💡 Custom configuration requires manually filling all necessary fields",
"officialHint": "💡 Official provider uses browser login, no API Key needed", "officialHint": "💡 Official provider uses browser login, no API Key needed",
"getApiKey": "Get API Key", "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}} *", "parameterConfig": "Parameter Config - {{name}} *",
"mainModel": "Main Model (optional)", "mainModel": "Main Model (optional)",
"mainModelPlaceholder": "e.g., GLM-4.6", "mainModelPlaceholder": "e.g., GLM-4.6",

View File

@@ -238,6 +238,9 @@
"customApiKeyHint": "💡 自定义配置需手动填写所有必要字段", "customApiKeyHint": "💡 自定义配置需手动填写所有必要字段",
"officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key", "officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key",
"getApiKey": "获取 API Key", "getApiKey": "获取 API Key",
"partnerPromotion": {
"zhipu": "智谱 GLM 是 CC Switch 的官方合作伙伴使用此链接充值可以获得9折优惠"
},
"parameterConfig": "参数配置 - {{name}} *", "parameterConfig": "参数配置 - {{name}} *",
"mainModel": "主模型 (可选)", "mainModel": "主模型 (可选)",
"mainModelPlaceholder": "例如: GLM-4.6", "mainModelPlaceholder": "例如: GLM-4.6",

View File

@@ -14,6 +14,8 @@ export interface Provider {
category?: ProviderCategory; category?: ProviderCategory;
createdAt?: number; // 添加时间戳(毫秒) createdAt?: number; // 添加时间戳(毫秒)
sortIndex?: number; // 排序索引(用于自定义拖拽排序) sortIndex?: number; // 排序索引(用于自定义拖拽排序)
// 新增:是否为商业合作伙伴
isPartner?: boolean;
// 可选:供应商元数据(仅存于 ~/.cc-switch/config.json不写入 live 配置) // 可选:供应商元数据(仅存于 ~/.cc-switch/config.json不写入 live 配置)
meta?: ProviderMeta; meta?: ProviderMeta;
} }