mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
feat(ui): add format transform configuration in provider form
- Add FormatTransformConfig type definition - Add format transform section in ProviderAdvancedConfig component - Support source/target format selection (Anthropic/OpenAI) - Add transform streaming toggle option
This commit is contained in:
@@ -8,19 +8,33 @@ import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
X,
|
||||
ArrowLeftRight,
|
||||
} from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ProviderTestConfig, ProviderProxyConfig } from "@/types";
|
||||
import type {
|
||||
ProviderTestConfig,
|
||||
ProviderProxyConfig,
|
||||
FormatTransformConfig,
|
||||
} from "@/types";
|
||||
|
||||
interface ProviderAdvancedConfigProps {
|
||||
testConfig: ProviderTestConfig;
|
||||
proxyConfig: ProviderProxyConfig;
|
||||
formatTransform?: FormatTransformConfig;
|
||||
onTestConfigChange: (config: ProviderTestConfig) => void;
|
||||
onProxyConfigChange: (config: ProviderProxyConfig) => void;
|
||||
onFormatTransformChange?: (config: FormatTransformConfig) => void;
|
||||
}
|
||||
|
||||
/** 从 ProviderProxyConfig 构建完整 URL */
|
||||
@@ -71,14 +85,19 @@ function parseProxyUrl(url: string): Partial<ProviderProxyConfig> {
|
||||
export function ProviderAdvancedConfig({
|
||||
testConfig,
|
||||
proxyConfig,
|
||||
formatTransform,
|
||||
onTestConfigChange,
|
||||
onProxyConfigChange,
|
||||
onFormatTransformChange,
|
||||
}: ProviderAdvancedConfigProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isTestConfigOpen, setIsTestConfigOpen] = useState(testConfig.enabled);
|
||||
const [isProxyConfigOpen, setIsProxyConfigOpen] = useState(
|
||||
proxyConfig.enabled,
|
||||
);
|
||||
const [isFormatTransformOpen, setIsFormatTransformOpen] = useState(
|
||||
formatTransform?.enabled ?? false,
|
||||
);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// 代理 URL 输入状态(仅在初始化时从 proxyConfig 构建)
|
||||
@@ -97,6 +116,11 @@ export function ProviderAdvancedConfig({
|
||||
setIsProxyConfigOpen(proxyConfig.enabled);
|
||||
}, [proxyConfig.enabled]);
|
||||
|
||||
// 同步外部 formatTransform.enabled 变化到展开状态
|
||||
useEffect(() => {
|
||||
setIsFormatTransformOpen(formatTransform?.enabled ?? false);
|
||||
}, [formatTransform?.enabled]);
|
||||
|
||||
// 仅在外部 proxyConfig 变化且非用户输入时同步(如:重置表单、加载数据)
|
||||
useEffect(() => {
|
||||
if (!isUserTyping) {
|
||||
@@ -450,6 +474,135 @@ export function ProviderAdvancedConfig({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 格式转换配置 */}
|
||||
{onFormatTransformChange && (
|
||||
<div className="rounded-lg border border-border/50 bg-muted/20">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between p-4 hover:bg-muted/30 transition-colors"
|
||||
onClick={() => setIsFormatTransformOpen(!isFormatTransformOpen)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<ArrowLeftRight className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium">
|
||||
{t("providerAdvanced.formatTransform")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Label
|
||||
htmlFor="format-transform-enabled"
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{t("providerAdvanced.enableFormatTransform")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="format-transform-enabled"
|
||||
checked={formatTransform?.enabled ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
onFormatTransformChange({
|
||||
...(formatTransform ?? { enabled: false }),
|
||||
enabled: checked,
|
||||
});
|
||||
if (checked) setIsFormatTransformOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isFormatTransformOpen ? (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-200",
|
||||
isFormatTransformOpen
|
||||
? "max-h-[500px] opacity-100"
|
||||
: "max-h-0 opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="border-t border-border/50 p-4 space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("providerAdvanced.formatTransformDesc")}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="source-format">
|
||||
{t("providerAdvanced.sourceFormat")}
|
||||
</Label>
|
||||
<Select
|
||||
value={formatTransform?.sourceFormat ?? "anthropic"}
|
||||
onValueChange={(value) =>
|
||||
onFormatTransformChange({
|
||||
...(formatTransform ?? { enabled: false }),
|
||||
sourceFormat: value as "anthropic" | "openai",
|
||||
})
|
||||
}
|
||||
disabled={!formatTransform?.enabled}
|
||||
>
|
||||
<SelectTrigger id="source-format">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="anthropic">
|
||||
Anthropic (Claude)
|
||||
</SelectItem>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="target-format">
|
||||
{t("providerAdvanced.targetFormat")}
|
||||
</Label>
|
||||
<Select
|
||||
value={formatTransform?.targetFormat ?? "openai"}
|
||||
onValueChange={(value) =>
|
||||
onFormatTransformChange({
|
||||
...(formatTransform ?? { enabled: false }),
|
||||
targetFormat: value as "anthropic" | "openai",
|
||||
})
|
||||
}
|
||||
disabled={!formatTransform?.enabled}
|
||||
>
|
||||
<SelectTrigger id="target-format">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="anthropic">
|
||||
Anthropic (Claude)
|
||||
</SelectItem>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="transform-streaming"
|
||||
checked={formatTransform?.transformStreaming ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
onFormatTransformChange({
|
||||
...(formatTransform ?? { enabled: false }),
|
||||
transformStreaming: checked,
|
||||
})
|
||||
}
|
||||
disabled={!formatTransform?.enabled}
|
||||
/>
|
||||
<Label htmlFor="transform-streaming" className="text-sm">
|
||||
{t("providerAdvanced.transformStreaming")}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ProviderMeta,
|
||||
ProviderTestConfig,
|
||||
ProviderProxyConfig,
|
||||
FormatTransformConfig,
|
||||
} from "@/types";
|
||||
import {
|
||||
providerPresets,
|
||||
@@ -168,6 +169,9 @@ export function ProviderForm({
|
||||
const [proxyConfig, setProxyConfig] = useState<ProviderProxyConfig>(
|
||||
() => initialData?.meta?.proxyConfig ?? { enabled: false },
|
||||
);
|
||||
const [formatTransform, setFormatTransform] = useState<FormatTransformConfig>(
|
||||
() => initialData?.meta?.formatTransform ?? { enabled: false },
|
||||
);
|
||||
|
||||
// 使用 category hook
|
||||
const { category } = useProviderCategory({
|
||||
@@ -940,6 +944,7 @@ export function ProviderForm({
|
||||
// 添加高级配置
|
||||
testConfig: testConfig.enabled ? testConfig : undefined,
|
||||
proxyConfig: proxyConfig.enabled ? proxyConfig : undefined,
|
||||
formatTransform: formatTransform.enabled ? formatTransform : undefined,
|
||||
};
|
||||
|
||||
onSubmit(payload);
|
||||
@@ -1464,8 +1469,10 @@ export function ProviderForm({
|
||||
<ProviderAdvancedConfig
|
||||
testConfig={testConfig}
|
||||
proxyConfig={proxyConfig}
|
||||
formatTransform={formatTransform}
|
||||
onTestConfigChange={setTestConfig}
|
||||
onProxyConfigChange={setProxyConfig}
|
||||
onFormatTransformChange={setFormatTransform}
|
||||
/>
|
||||
|
||||
{showButtons && (
|
||||
|
||||
@@ -119,6 +119,18 @@ export interface ProviderProxyConfig {
|
||||
proxyPassword?: string;
|
||||
}
|
||||
|
||||
// 格式转换配置(用于 OpenRouter 等需要 API 格式转换的供应商)
|
||||
export interface FormatTransformConfig {
|
||||
// 是否启用格式转换
|
||||
enabled: boolean;
|
||||
// 源格式:anthropic, openai, gemini
|
||||
sourceFormat?: "anthropic" | "openai" | "gemini";
|
||||
// 目标格式:anthropic, openai, gemini
|
||||
targetFormat?: "anthropic" | "openai" | "gemini";
|
||||
// 是否转换流式响应(默认 true)
|
||||
transformStreaming?: boolean;
|
||||
}
|
||||
|
||||
// 供应商元数据(字段名与后端一致,保持 snake_case)
|
||||
export interface ProviderMeta {
|
||||
// 自定义端点:以 URL 为键,值为端点信息
|
||||
@@ -135,6 +147,8 @@ export interface ProviderMeta {
|
||||
testConfig?: ProviderTestConfig;
|
||||
// 供应商单独的代理配置
|
||||
proxyConfig?: ProviderProxyConfig;
|
||||
// 格式转换配置(用于 OpenRouter 等需要 API 格式转换的供应商)
|
||||
formatTransform?: FormatTransformConfig;
|
||||
}
|
||||
|
||||
// Skill 同步方式
|
||||
|
||||
Reference in New Issue
Block a user