mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
8fe5c1041a
* feat: add Universal Provider feature - Add Universal Provider data structures and type definitions - Implement backend CRUD operations and sync functionality - Add frontend UI components (UniversalProviderPanel, Card, FormModal) - Add NewAPI icon and preset configuration - Support cross-app (Claude/Codex/Gemini) configuration sync - Add website URL field for providers - Implement real-time refresh via event notifications - Add i18n support (Chinese/English/Japanese) * feat: integrate universal provider presets into add provider dialog - Add universal provider presets (NewAPI, Custom Gateway) to preset selector - Show universal presets with Layers icon badge in preset selector - Open UniversalProviderFormModal when universal preset is clicked - Pass initialPreset to auto-fill form when opened from add dialog - Add i18n keys for addSuccess/addFailed messages - Keep separate universal provider panel for management * refactor: move universal provider management to add dialog - Remove Layers button from main navigation header - Add 'Manage' button next to universal provider presets - Open UniversalProviderPanel from within add provider dialog - Add i18n keys for 'manage' in all locales * style: display universal provider presets on separate line - Move universal provider section to a new row with border separator - Add label '统一供应商:' to clarify the section * style: unify universal provider label style with preset label - Use FormLabel component for consistent styling - Add background to 'Manage' button matching preset buttons - Update icon size and button padding for consistency * feat: add sync functionality and JSON preview for Universal Provider * fix: add missing in_failover_queue field to Provider structs After rebasing to main, the Provider struct gained a new `in_failover_queue` field. This fix adds the missing field to the three to_*_provider() methods in UniversalProvider. * refactor: redesign AddProviderDialog with tab-based layout - Add tabs to separate app-specific providers and universal providers - Move "Add Universal Provider" button from panel header to footer - Remove unused handleAdd callback and clean up imports - Update emptyHint i18n text to reference the footer button * fix: append /v1 suffix to Codex base_url in Universal Provider Codex uses OpenAI-compatible API which requires the /v1 endpoint suffix. The Universal Provider now automatically appends /v1 to base_url when generating Codex provider config if not already present. - Handle trailing slashes to avoid double slashes - Apply fix to both backend (to_codex_provider) and frontend preview * feat: auto-sync universal provider to apps on creation Previously, users had to manually click sync after adding a universal provider. Now it automatically syncs to Claude/Codex/Gemini on creation, providing a smoother user experience. --------- Co-authored-by: Jason <farion1231@gmail.com>
223 lines
8.1 KiB
TypeScript
223 lines
8.1 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { FormLabel } from "@/components/ui/form";
|
|
import { ClaudeIcon, CodexIcon, GeminiIcon } from "@/components/BrandIcons";
|
|
import { Zap, Star, Layers, Settings2 } from "lucide-react";
|
|
import type { ProviderPreset } from "@/config/claudeProviderPresets";
|
|
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
|
|
import type { GeminiProviderPreset } from "@/config/geminiProviderPresets";
|
|
import type { ProviderCategory } from "@/types";
|
|
import {
|
|
universalProviderPresets,
|
|
type UniversalProviderPreset,
|
|
} from "@/config/universalProviderPresets";
|
|
import { ProviderIcon } from "@/components/ProviderIcon";
|
|
|
|
type PresetEntry = {
|
|
id: string;
|
|
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset;
|
|
};
|
|
|
|
interface ProviderPresetSelectorProps {
|
|
selectedPresetId: string | null;
|
|
groupedPresets: Record<string, PresetEntry[]>;
|
|
categoryKeys: string[];
|
|
presetCategoryLabels: Record<string, string>;
|
|
onPresetChange: (value: string) => void;
|
|
onUniversalPresetSelect?: (preset: UniversalProviderPreset) => void;
|
|
onManageUniversalProviders?: () => void;
|
|
category?: ProviderCategory; // 当前选中的分类
|
|
}
|
|
|
|
export function ProviderPresetSelector({
|
|
selectedPresetId,
|
|
groupedPresets,
|
|
categoryKeys,
|
|
presetCategoryLabels,
|
|
onPresetChange,
|
|
onUniversalPresetSelect,
|
|
onManageUniversalProviders,
|
|
category,
|
|
}: ProviderPresetSelectorProps) {
|
|
const { t } = useTranslation();
|
|
|
|
// 根据分类获取提示文字
|
|
const getCategoryHint = (): React.ReactNode => {
|
|
switch (category) {
|
|
case "official":
|
|
return t("providerForm.officialHint", {
|
|
defaultValue: "💡 官方供应商使用浏览器登录,无需配置 API Key",
|
|
});
|
|
case "cn_official":
|
|
return t("providerForm.cnOfficialApiKeyHint", {
|
|
defaultValue: "💡 国产官方供应商只需填写 API Key,请求地址已预设",
|
|
});
|
|
case "aggregator":
|
|
return t("providerForm.aggregatorApiKeyHint", {
|
|
defaultValue: "💡 聚合服务供应商只需填写 API Key 即可使用",
|
|
});
|
|
case "third_party":
|
|
return t("providerForm.thirdPartyApiKeyHint", {
|
|
defaultValue: "💡 第三方供应商需要填写 API Key 和请求地址",
|
|
});
|
|
case "custom":
|
|
return t("providerForm.customApiKeyHint", {
|
|
defaultValue: "💡 自定义配置需手动填写所有必要字段",
|
|
});
|
|
default:
|
|
return t("providerPreset.hint", {
|
|
defaultValue: "选择预设后可继续调整下方字段。",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 渲染预设按钮的图标
|
|
const renderPresetIcon = (
|
|
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
|
) => {
|
|
const iconType = preset.theme?.icon;
|
|
if (!iconType) return null;
|
|
|
|
switch (iconType) {
|
|
case "claude":
|
|
return <ClaudeIcon size={14} />;
|
|
case "codex":
|
|
return <CodexIcon size={14} />;
|
|
case "gemini":
|
|
return <GeminiIcon size={14} />;
|
|
case "generic":
|
|
return <Zap size={14} />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// 获取预设按钮的样式类名
|
|
const getPresetButtonClass = (
|
|
isSelected: boolean,
|
|
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
|
) => {
|
|
const baseClass =
|
|
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
|
|
|
if (isSelected) {
|
|
// 如果有自定义主题,使用自定义颜色
|
|
if (preset.theme?.backgroundColor) {
|
|
return `${baseClass} text-white`;
|
|
}
|
|
// 默认使用主题蓝色
|
|
return `${baseClass} bg-blue-500 text-white dark:bg-blue-600`;
|
|
}
|
|
|
|
return `${baseClass} bg-accent text-muted-foreground hover:bg-accent/80`;
|
|
};
|
|
|
|
// 获取预设按钮的内联样式(用于自定义背景色)
|
|
const getPresetButtonStyle = (
|
|
isSelected: boolean,
|
|
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
|
) => {
|
|
if (!isSelected || !preset.theme?.backgroundColor) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
backgroundColor: preset.theme.backgroundColor,
|
|
color: preset.theme.textColor || "#FFFFFF",
|
|
};
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<FormLabel>{t("providerPreset.label")}</FormLabel>
|
|
<div className="flex flex-wrap gap-2">
|
|
{/* 自定义按钮 */}
|
|
<button
|
|
type="button"
|
|
onClick={() => onPresetChange("custom")}
|
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
selectedPresetId === "custom"
|
|
? "bg-blue-500 text-white dark:bg-blue-600"
|
|
: "bg-accent text-muted-foreground hover:bg-accent/80"
|
|
}`}
|
|
>
|
|
{t("providerPreset.custom")}
|
|
</button>
|
|
|
|
{/* 预设按钮 */}
|
|
{categoryKeys.map((category) => {
|
|
const entries = groupedPresets[category];
|
|
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)} relative`}
|
|
style={getPresetButtonStyle(isSelected, entry.preset)}
|
|
title={
|
|
presetCategoryLabels[category] ?? t("providerPreset.other")
|
|
}
|
|
>
|
|
{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>
|
|
);
|
|
});
|
|
})}
|
|
</div>
|
|
|
|
{/* 统一供应商预设(新的一行) */}
|
|
{onUniversalPresetSelect && universalProviderPresets.length > 0 && (
|
|
<>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{universalProviderPresets.map((preset) => (
|
|
<button
|
|
key={`universal-${preset.providerType}`}
|
|
type="button"
|
|
onClick={() => onUniversalPresetSelect(preset)}
|
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-accent text-muted-foreground hover:bg-accent/80 relative"
|
|
title={t("universalProvider.hint", {
|
|
defaultValue:
|
|
"跨应用统一配置,自动同步到 Claude/Codex/Gemini",
|
|
})}
|
|
>
|
|
<ProviderIcon icon={preset.icon} name={preset.name} size={14} />
|
|
{preset.name}
|
|
<span className="absolute -top-1 -right-1 flex items-center gap-0.5 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-md">
|
|
<Layers className="h-2.5 w-2.5" />
|
|
</span>
|
|
</button>
|
|
))}
|
|
{/* 管理统一供应商按钮 */}
|
|
{onManageUniversalProviders && (
|
|
<button
|
|
type="button"
|
|
onClick={onManageUniversalProviders}
|
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-accent text-muted-foreground hover:bg-accent/80"
|
|
title={t("universalProvider.manage", {
|
|
defaultValue: "管理统一供应商",
|
|
})}
|
|
>
|
|
<Settings2 className="h-4 w-4" />
|
|
{t("universalProvider.manage", {
|
|
defaultValue: "管理",
|
|
})}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<p className="text-xs text-muted-foreground">{getCategoryHint()}</p>
|
|
</div>
|
|
);
|
|
}
|