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;
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}
/>
)}

View File

@@ -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",

View File

@@ -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;
};

View File

@@ -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 (
<button
key={entry.id}
type="button"
onClick={() => onPresetChange(entry.id)}
className={getPresetButtonClass(isSelected, entry.preset)}
className={`${getPresetButtonClass(isSelected, entry.preset)} relative`}
style={getPresetButtonStyle(isSelected, entry.preset)}
title={
presetCategoryLabels[category] ??
@@ -173,6 +174,11 @@ export function ProviderPresetSelector({
>
{renderPresetIcon(entry.preset)}
{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>
);
});

View File

@@ -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,
};
}

View File

@@ -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 && (
<div className="-mt-1 pl-1">
<div className="space-y-2 -mt-1 pl-1">
<a
href={websiteUrl}
target="_blank"
@@ -68,6 +72,18 @@ export function ApiKeySection({
defaultValue: "获取 API Key",
})}
</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>