2025-10-16 13:02:38 +08:00
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
2025-10-16 10:49:56 +08:00
|
|
|
|
import { useForm } from "react-hook-form";
|
|
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Form,
|
|
|
|
|
|
FormControl,
|
|
|
|
|
|
FormField,
|
|
|
|
|
|
FormItem,
|
|
|
|
|
|
FormLabel,
|
|
|
|
|
|
FormMessage,
|
|
|
|
|
|
} from "@/components/ui/form";
|
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
2025-10-16 12:13:51 +08:00
|
|
|
|
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
|
2025-10-16 13:02:38 +08:00
|
|
|
|
import type { AppType } from "@/lib/api";
|
2025-10-16 19:40:22 +08:00
|
|
|
|
import type { ProviderCategory, CustomEndpoint } from "@/types";
|
2025-10-16 15:32:26 +08:00
|
|
|
|
import { providerPresets, type ProviderPreset } from "@/config/providerPresets";
|
2025-10-16 13:02:38 +08:00
|
|
|
|
import {
|
|
|
|
|
|
codexProviderPresets,
|
|
|
|
|
|
type CodexProviderPreset,
|
|
|
|
|
|
} from "@/config/codexProviderPresets";
|
|
|
|
|
|
import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
2025-10-16 17:40:25 +08:00
|
|
|
|
import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput";
|
2025-10-16 17:44:23 +08:00
|
|
|
|
import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest";
|
2025-10-16 18:50:44 +08:00
|
|
|
|
import CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor";
|
2025-10-16 20:21:42 +08:00
|
|
|
|
import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector";
|
2025-10-16 20:32:11 +08:00
|
|
|
|
import { CommonConfigEditor } from "./CommonConfigEditor";
|
2025-10-16 17:44:23 +08:00
|
|
|
|
import { Zap } from "lucide-react";
|
2025-10-16 18:50:44 +08:00
|
|
|
|
import {
|
|
|
|
|
|
useProviderCategory,
|
|
|
|
|
|
useApiKeyState,
|
|
|
|
|
|
useBaseUrlState,
|
|
|
|
|
|
useModelState,
|
|
|
|
|
|
useCodexConfigState,
|
2025-10-16 19:56:00 +08:00
|
|
|
|
useApiKeyLink,
|
|
|
|
|
|
useCustomEndpoints,
|
2025-10-16 20:21:42 +08:00
|
|
|
|
useKimiModelSelector,
|
2025-10-16 20:25:39 +08:00
|
|
|
|
useTemplateValues,
|
2025-10-16 20:32:11 +08:00
|
|
|
|
useCommonConfigSnippet,
|
2025-10-16 18:50:44 +08:00
|
|
|
|
} from "./hooks";
|
2025-10-16 13:02:38 +08:00
|
|
|
|
|
|
|
|
|
|
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
|
2025-10-16 15:32:26 +08:00
|
|
|
|
const CODEX_DEFAULT_CONFIG = JSON.stringify({ auth: {}, config: "" }, null, 2);
|
2025-10-16 13:02:38 +08:00
|
|
|
|
|
|
|
|
|
|
type PresetEntry = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
preset: ProviderPreset | CodexProviderPreset;
|
|
|
|
|
|
};
|
2025-10-16 10:49:56 +08:00
|
|
|
|
|
|
|
|
|
|
interface ProviderFormProps {
|
2025-10-16 13:02:38 +08:00
|
|
|
|
appType: AppType;
|
2025-10-16 10:49:56 +08:00
|
|
|
|
submitLabel: string;
|
2025-10-16 13:02:38 +08:00
|
|
|
|
onSubmit: (values: ProviderFormValues) => void;
|
2025-10-16 10:49:56 +08:00
|
|
|
|
onCancel: () => void;
|
|
|
|
|
|
initialData?: {
|
|
|
|
|
|
name?: string;
|
|
|
|
|
|
websiteUrl?: string;
|
|
|
|
|
|
settingsConfig?: Record<string, unknown>;
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function ProviderForm({
|
2025-10-16 13:02:38 +08:00
|
|
|
|
appType,
|
2025-10-16 10:49:56 +08:00
|
|
|
|
submitLabel,
|
|
|
|
|
|
onSubmit,
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
initialData,
|
|
|
|
|
|
}: ProviderFormProps) {
|
|
|
|
|
|
const { t } = useTranslation();
|
2025-10-16 17:40:25 +08:00
|
|
|
|
const isEditMode = Boolean(initialData);
|
|
|
|
|
|
|
2025-10-16 16:51:47 +08:00
|
|
|
|
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(
|
2025-10-16 20:21:42 +08:00
|
|
|
|
initialData ? null : "custom"
|
2025-10-16 16:51:47 +08:00
|
|
|
|
);
|
2025-10-16 13:02:38 +08:00
|
|
|
|
const [activePreset, setActivePreset] = useState<{
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
category?: ProviderCategory;
|
|
|
|
|
|
} | null>(null);
|
2025-10-16 17:44:23 +08:00
|
|
|
|
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
2025-10-16 13:02:38 +08:00
|
|
|
|
|
2025-10-16 19:40:22 +08:00
|
|
|
|
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
|
2025-10-16 20:21:42 +08:00
|
|
|
|
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
|
|
|
|
|
|
[]
|
|
|
|
|
|
);
|
2025-10-16 19:40:22 +08:00
|
|
|
|
|
2025-10-16 17:40:25 +08:00
|
|
|
|
// 使用 category hook
|
|
|
|
|
|
const { category } = useProviderCategory({
|
|
|
|
|
|
appType,
|
|
|
|
|
|
selectedPresetId,
|
|
|
|
|
|
isEditMode,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 13:02:38 +08:00
|
|
|
|
useEffect(() => {
|
2025-10-16 16:51:47 +08:00
|
|
|
|
setSelectedPresetId(initialData ? null : "custom");
|
2025-10-16 13:02:38 +08:00
|
|
|
|
setActivePreset(null);
|
|
|
|
|
|
}, [appType, initialData]);
|
2025-10-16 10:49:56 +08:00
|
|
|
|
|
|
|
|
|
|
const defaultValues: ProviderFormData = useMemo(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
name: initialData?.name ?? "",
|
|
|
|
|
|
websiteUrl: initialData?.websiteUrl ?? "",
|
|
|
|
|
|
settingsConfig: initialData?.settingsConfig
|
|
|
|
|
|
? JSON.stringify(initialData.settingsConfig, null, 2)
|
2025-10-16 13:02:38 +08:00
|
|
|
|
: appType === "codex"
|
|
|
|
|
|
? CODEX_DEFAULT_CONFIG
|
|
|
|
|
|
: CLAUDE_DEFAULT_CONFIG,
|
2025-10-16 10:49:56 +08:00
|
|
|
|
}),
|
2025-10-16 20:21:42 +08:00
|
|
|
|
[initialData, appType]
|
2025-10-16 10:49:56 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const form = useForm<ProviderFormData>({
|
|
|
|
|
|
resolver: zodResolver(providerSchema),
|
|
|
|
|
|
defaultValues,
|
|
|
|
|
|
mode: "onSubmit",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 17:40:25 +08:00
|
|
|
|
// 使用 API Key hook
|
2025-10-16 20:21:42 +08:00
|
|
|
|
const {
|
|
|
|
|
|
apiKey,
|
|
|
|
|
|
handleApiKeyChange,
|
|
|
|
|
|
showApiKey: shouldShowApiKey,
|
|
|
|
|
|
} = useApiKeyState({
|
2025-10-16 17:40:25 +08:00
|
|
|
|
initialConfig: form.watch("settingsConfig"),
|
|
|
|
|
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
|
|
|
|
|
selectedPresetId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 17:44:23 +08:00
|
|
|
|
// 使用 Base URL hook
|
|
|
|
|
|
const {
|
|
|
|
|
|
baseUrl,
|
|
|
|
|
|
// codexBaseUrl, // TODO: 等 Codex 支持时使用
|
|
|
|
|
|
handleClaudeBaseUrlChange,
|
|
|
|
|
|
// handleCodexBaseUrlChange, // TODO: 等 Codex 支持时使用
|
|
|
|
|
|
} = useBaseUrlState({
|
|
|
|
|
|
appType,
|
|
|
|
|
|
category,
|
|
|
|
|
|
settingsConfig: form.watch("settingsConfig"),
|
|
|
|
|
|
codexConfig: "", // TODO: 从 settingsConfig 中提取 codex config
|
|
|
|
|
|
onSettingsConfigChange: (config) => form.setValue("settingsConfig", config),
|
|
|
|
|
|
onCodexConfigChange: () => {
|
|
|
|
|
|
// TODO: 更新 codex config
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 17:58:49 +08:00
|
|
|
|
// 使用 Model hook
|
2025-10-16 20:21:42 +08:00
|
|
|
|
const { claudeModel, claudeSmallFastModel, handleModelChange } =
|
|
|
|
|
|
useModelState({
|
|
|
|
|
|
settingsConfig: form.watch("settingsConfig"),
|
|
|
|
|
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
|
|
|
|
|
});
|
2025-10-16 17:58:49 +08:00
|
|
|
|
|
2025-10-16 18:50:44 +08:00
|
|
|
|
// 使用 Codex 配置 hook (仅 Codex 模式)
|
|
|
|
|
|
const {
|
|
|
|
|
|
codexAuth,
|
|
|
|
|
|
codexConfig,
|
|
|
|
|
|
codexApiKey,
|
|
|
|
|
|
codexBaseUrl,
|
|
|
|
|
|
codexAuthError,
|
|
|
|
|
|
setCodexAuth,
|
|
|
|
|
|
handleCodexApiKeyChange,
|
|
|
|
|
|
handleCodexBaseUrlChange,
|
|
|
|
|
|
handleCodexConfigChange,
|
|
|
|
|
|
resetCodexConfig,
|
|
|
|
|
|
} = useCodexConfigState({ initialData });
|
|
|
|
|
|
|
2025-10-16 20:21:42 +08:00
|
|
|
|
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
|
|
|
|
|
|
useState(false);
|
2025-10-16 18:50:44 +08:00
|
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
form.reset(defaultValues);
|
|
|
|
|
|
}, [defaultValues, form]);
|
|
|
|
|
|
|
2025-10-16 20:21:42 +08:00
|
|
|
|
const presetCategoryLabels: Record<string, string> = useMemo(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
official: t("providerPreset.categoryOfficial", {
|
|
|
|
|
|
defaultValue: "官方",
|
|
|
|
|
|
}),
|
|
|
|
|
|
cn_official: t("providerPreset.categoryCnOfficial", {
|
|
|
|
|
|
defaultValue: "国内官方",
|
|
|
|
|
|
}),
|
|
|
|
|
|
aggregator: t("providerPreset.categoryAggregator", {
|
|
|
|
|
|
defaultValue: "聚合服务",
|
|
|
|
|
|
}),
|
|
|
|
|
|
third_party: t("providerPreset.categoryThirdParty", {
|
|
|
|
|
|
defaultValue: "第三方",
|
|
|
|
|
|
}),
|
|
|
|
|
|
}),
|
|
|
|
|
|
[t]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const presetEntries = useMemo(() => {
|
|
|
|
|
|
if (appType === "codex") {
|
|
|
|
|
|
return codexProviderPresets.map<PresetEntry>((preset, index) => ({
|
|
|
|
|
|
id: `codex-${index}`,
|
|
|
|
|
|
preset,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
return providerPresets.map<PresetEntry>((preset, index) => ({
|
|
|
|
|
|
id: `claude-${index}`,
|
|
|
|
|
|
preset,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}, [appType]);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 Kimi 模型选择器 hook
|
|
|
|
|
|
const {
|
|
|
|
|
|
shouldShow: shouldShowKimiSelector,
|
|
|
|
|
|
kimiAnthropicModel,
|
|
|
|
|
|
kimiAnthropicSmallFastModel,
|
|
|
|
|
|
handleKimiModelChange,
|
|
|
|
|
|
} = useKimiModelSelector({
|
|
|
|
|
|
initialData,
|
|
|
|
|
|
settingsConfig: form.watch("settingsConfig"),
|
|
|
|
|
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
|
|
|
|
|
selectedPresetId,
|
|
|
|
|
|
presetName:
|
|
|
|
|
|
selectedPresetId && selectedPresetId !== "custom"
|
|
|
|
|
|
? presetEntries.find((item) => item.id === selectedPresetId)?.preset
|
|
|
|
|
|
.name || ""
|
|
|
|
|
|
: "",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 20:25:39 +08:00
|
|
|
|
// 使用模板变量 hook (仅 Claude 模式)
|
|
|
|
|
|
const {
|
|
|
|
|
|
templateValues,
|
|
|
|
|
|
templateValueEntries,
|
|
|
|
|
|
selectedPreset: templatePreset,
|
|
|
|
|
|
handleTemplateValueChange,
|
|
|
|
|
|
validateTemplateValues,
|
|
|
|
|
|
} = useTemplateValues({
|
|
|
|
|
|
selectedPresetId: appType === "claude" ? selectedPresetId : null,
|
|
|
|
|
|
presetEntries: appType === "claude" ? presetEntries : [],
|
|
|
|
|
|
settingsConfig: form.watch("settingsConfig"),
|
|
|
|
|
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 20:32:11 +08:00
|
|
|
|
// 使用通用配置片段 hook (仅 Claude 模式)
|
|
|
|
|
|
const {
|
|
|
|
|
|
useCommonConfig,
|
|
|
|
|
|
commonConfigSnippet,
|
|
|
|
|
|
commonConfigError,
|
|
|
|
|
|
handleCommonConfigToggle,
|
|
|
|
|
|
handleCommonConfigSnippetChange,
|
|
|
|
|
|
} = useCommonConfigSnippet({
|
|
|
|
|
|
settingsConfig: form.watch("settingsConfig"),
|
|
|
|
|
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
|
|
|
|
|
initialData: appType === "claude" ? initialData : undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
|
|
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
|
const handleSubmit = (values: ProviderFormData) => {
|
2025-10-16 20:25:39 +08:00
|
|
|
|
// 验证模板变量(仅 Claude 模式)
|
|
|
|
|
|
if (appType === "claude" && templateValueEntries.length > 0) {
|
|
|
|
|
|
const validation = validateTemplateValues();
|
|
|
|
|
|
if (!validation.isValid && validation.missingField) {
|
|
|
|
|
|
form.setError("settingsConfig", {
|
|
|
|
|
|
type: "manual",
|
|
|
|
|
|
message: t("providerForm.fillParameter", {
|
|
|
|
|
|
label: validation.missingField.label,
|
|
|
|
|
|
defaultValue: `请填写 ${validation.missingField.label}`,
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 18:50:44 +08:00
|
|
|
|
let settingsConfig: string;
|
|
|
|
|
|
|
|
|
|
|
|
// Codex: 组合 auth 和 config
|
|
|
|
|
|
if (appType === "codex") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const authJson = JSON.parse(codexAuth);
|
|
|
|
|
|
const configObj = {
|
|
|
|
|
|
auth: authJson,
|
|
|
|
|
|
config: codexConfig ?? "",
|
|
|
|
|
|
};
|
|
|
|
|
|
settingsConfig = JSON.stringify(configObj);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// 如果解析失败,使用表单中的配置
|
|
|
|
|
|
settingsConfig = values.settingsConfig.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Claude: 使用表单配置
|
|
|
|
|
|
settingsConfig = values.settingsConfig.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 13:02:38 +08:00
|
|
|
|
const payload: ProviderFormValues = {
|
2025-10-16 10:49:56 +08:00
|
|
|
|
...values,
|
2025-10-16 13:02:38 +08:00
|
|
|
|
name: values.name.trim(),
|
2025-10-16 10:49:56 +08:00
|
|
|
|
websiteUrl: values.websiteUrl?.trim() ?? "",
|
2025-10-16 18:50:44 +08:00
|
|
|
|
settingsConfig,
|
2025-10-16 13:02:38 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (activePreset) {
|
|
|
|
|
|
payload.presetId = activePreset.id;
|
|
|
|
|
|
if (activePreset.category) {
|
|
|
|
|
|
payload.presetCategory = activePreset.category;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 19:56:00 +08:00
|
|
|
|
// 新建供应商时:添加自定义端点
|
|
|
|
|
|
if (!initialData && customEndpointsMap) {
|
|
|
|
|
|
payload.meta = { custom_endpoints: customEndpointsMap };
|
2025-10-16 19:40:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 13:02:38 +08:00
|
|
|
|
onSubmit(payload);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const groupedPresets = useMemo(() => {
|
|
|
|
|
|
return presetEntries.reduce<Record<string, PresetEntry[]>>((acc, entry) => {
|
|
|
|
|
|
const category = entry.preset.category ?? "others";
|
|
|
|
|
|
if (!acc[category]) {
|
|
|
|
|
|
acc[category] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
acc[category].push(entry);
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
}, [presetEntries]);
|
|
|
|
|
|
|
|
|
|
|
|
const categoryKeys = useMemo(() => {
|
|
|
|
|
|
return Object.keys(groupedPresets).filter(
|
2025-10-16 20:21:42 +08:00
|
|
|
|
(key) => key !== "custom" && groupedPresets[key]?.length
|
2025-10-16 13:02:38 +08:00
|
|
|
|
);
|
|
|
|
|
|
}, [groupedPresets]);
|
|
|
|
|
|
|
2025-10-16 17:44:23 +08:00
|
|
|
|
// 判断是否显示端点测速(仅第三方和自定义类别)
|
|
|
|
|
|
const shouldShowSpeedTest =
|
|
|
|
|
|
category === "third_party" || category === "custom";
|
|
|
|
|
|
|
2025-10-16 19:56:00 +08:00
|
|
|
|
// 使用 API Key 链接 hook (Claude)
|
|
|
|
|
|
const {
|
|
|
|
|
|
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
|
|
|
|
|
|
websiteUrl: claudeWebsiteUrl,
|
|
|
|
|
|
} = useApiKeyLink({
|
|
|
|
|
|
appType: "claude",
|
|
|
|
|
|
category,
|
|
|
|
|
|
selectedPresetId,
|
|
|
|
|
|
presetEntries,
|
|
|
|
|
|
formWebsiteUrl: form.watch("websiteUrl") || "",
|
|
|
|
|
|
});
|
2025-10-16 19:37:43 +08:00
|
|
|
|
|
2025-10-16 19:56:00 +08:00
|
|
|
|
// 使用 API Key 链接 hook (Codex)
|
|
|
|
|
|
const {
|
|
|
|
|
|
shouldShowApiKeyLink: shouldShowCodexApiKeyLink,
|
|
|
|
|
|
websiteUrl: codexWebsiteUrl,
|
|
|
|
|
|
} = useApiKeyLink({
|
|
|
|
|
|
appType: "codex",
|
|
|
|
|
|
category,
|
|
|
|
|
|
selectedPresetId,
|
|
|
|
|
|
presetEntries,
|
|
|
|
|
|
formWebsiteUrl: form.watch("websiteUrl") || "",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 使用自定义端点 hook
|
|
|
|
|
|
const customEndpointsMap = useCustomEndpoints({
|
|
|
|
|
|
appType,
|
|
|
|
|
|
selectedPresetId,
|
|
|
|
|
|
presetEntries,
|
|
|
|
|
|
draftCustomEndpoints,
|
|
|
|
|
|
baseUrl,
|
|
|
|
|
|
codexBaseUrl,
|
|
|
|
|
|
});
|
2025-10-16 19:37:43 +08:00
|
|
|
|
|
2025-10-16 13:02:38 +08:00
|
|
|
|
const handlePresetChange = (value: string) => {
|
|
|
|
|
|
setSelectedPresetId(value);
|
|
|
|
|
|
if (value === "custom") {
|
|
|
|
|
|
setActivePreset(null);
|
2025-10-16 16:51:47 +08:00
|
|
|
|
form.reset(defaultValues);
|
2025-10-16 18:50:44 +08:00
|
|
|
|
|
|
|
|
|
|
// Codex 自定义模式:重置为空配置
|
|
|
|
|
|
if (appType === "codex") {
|
|
|
|
|
|
resetCodexConfig({}, "");
|
|
|
|
|
|
}
|
2025-10-16 13:02:38 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const entry = presetEntries.find((item) => item.id === value);
|
|
|
|
|
|
if (!entry) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setActivePreset({
|
|
|
|
|
|
id: value,
|
|
|
|
|
|
category: entry.preset.category,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (appType === "codex") {
|
|
|
|
|
|
const preset = entry.preset as CodexProviderPreset;
|
2025-10-16 18:50:44 +08:00
|
|
|
|
const auth = preset.auth ?? {};
|
|
|
|
|
|
const config = preset.config ?? "";
|
2025-10-16 13:02:38 +08:00
|
|
|
|
|
2025-10-16 18:50:44 +08:00
|
|
|
|
// 重置 Codex 配置
|
|
|
|
|
|
resetCodexConfig(auth, config);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新表单其他字段
|
2025-10-16 13:02:38 +08:00
|
|
|
|
form.reset({
|
|
|
|
|
|
name: preset.name,
|
|
|
|
|
|
websiteUrl: preset.websiteUrl ?? "",
|
2025-10-16 18:50:44 +08:00
|
|
|
|
settingsConfig: JSON.stringify({ auth, config }, null, 2),
|
2025-10-16 13:02:38 +08:00
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const preset = entry.preset as ProviderPreset;
|
|
|
|
|
|
const config = applyTemplateValues(
|
|
|
|
|
|
preset.settingsConfig,
|
2025-10-16 20:21:42 +08:00
|
|
|
|
preset.templateValues
|
2025-10-16 13:02:38 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
form.reset({
|
|
|
|
|
|
name: preset.name,
|
|
|
|
|
|
websiteUrl: preset.websiteUrl ?? "",
|
|
|
|
|
|
settingsConfig: JSON.stringify(config, null, 2),
|
2025-10-16 10:49:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form {...form}>
|
2025-10-16 12:13:51 +08:00
|
|
|
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
2025-10-16 16:51:47 +08:00
|
|
|
|
{/* 预设供应商选择(仅新增模式显示) */}
|
|
|
|
|
|
{!initialData && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<FormLabel>
|
|
|
|
|
|
{t("providerPreset.label", { defaultValue: "预设供应商" })}
|
|
|
|
|
|
</FormLabel>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{/* 自定义按钮 */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handlePresetChange("custom")}
|
|
|
|
|
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
|
|
|
|
selectedPresetId === "custom"
|
|
|
|
|
|
? "bg-emerald-500 text-white dark:bg-emerald-600"
|
|
|
|
|
|
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
2025-10-16 13:02:38 +08:00
|
|
|
|
{t("providerPreset.custom", { defaultValue: "自定义配置" })}
|
2025-10-16 16:51:47 +08:00
|
|
|
|
</button>
|
2025-10-16 13:02:38 +08:00
|
|
|
|
|
2025-10-16 16:51:47 +08:00
|
|
|
|
{/* 预设按钮 */}
|
2025-10-16 13:02:38 +08:00
|
|
|
|
{categoryKeys.map((category) => {
|
|
|
|
|
|
const entries = groupedPresets[category];
|
|
|
|
|
|
if (!entries || entries.length === 0) return null;
|
2025-10-16 16:51:47 +08:00
|
|
|
|
return entries.map((entry) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={entry.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handlePresetChange(entry.id)}
|
|
|
|
|
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
|
|
|
|
selectedPresetId === entry.id
|
|
|
|
|
|
? "bg-emerald-500 text-white dark:bg-emerald-600"
|
|
|
|
|
|
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
title={
|
|
|
|
|
|
presetCategoryLabels[category] ??
|
|
|
|
|
|
t("providerPreset.categoryOther", {
|
|
|
|
|
|
defaultValue: "其他",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{entry.preset.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
));
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
{t("providerPreset.helper", {
|
|
|
|
|
|
defaultValue: "选择预设后可继续调整下方字段。",
|
2025-10-16 13:02:38 +08:00
|
|
|
|
})}
|
2025-10-16 16:51:47 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-16 13:02:38 +08:00
|
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
|
<FormField
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
|
<FormItem>
|
|
|
|
|
|
<FormLabel>
|
|
|
|
|
|
{t("provider.name", { defaultValue: "供应商名称" })}
|
|
|
|
|
|
</FormLabel>
|
|
|
|
|
|
<FormControl>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
{...field}
|
|
|
|
|
|
placeholder={t("provider.namePlaceholder", {
|
|
|
|
|
|
defaultValue: "例如:Claude 官方",
|
|
|
|
|
|
})}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FormControl>
|
|
|
|
|
|
<FormMessage />
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
name="websiteUrl"
|
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
|
<FormItem>
|
|
|
|
|
|
<FormLabel>
|
|
|
|
|
|
{t("provider.websiteUrl", { defaultValue: "官网链接" })}
|
|
|
|
|
|
</FormLabel>
|
|
|
|
|
|
<FormControl>
|
2025-10-16 12:13:51 +08:00
|
|
|
|
<Input {...field} placeholder="https://" />
|
2025-10-16 10:49:56 +08:00
|
|
|
|
</FormControl>
|
|
|
|
|
|
<FormMessage />
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-10-16 17:40:25 +08:00
|
|
|
|
{/* API Key 输入框(仅 Claude 且非编辑模式显示) */}
|
2025-10-16 20:21:42 +08:00
|
|
|
|
{appType === "claude" &&
|
|
|
|
|
|
shouldShowApiKey(form.watch("settingsConfig"), isEditMode) && (
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<ApiKeyInput
|
|
|
|
|
|
value={apiKey}
|
|
|
|
|
|
onChange={handleApiKeyChange}
|
|
|
|
|
|
required={category !== "official"}
|
|
|
|
|
|
placeholder={
|
|
|
|
|
|
category === "official"
|
|
|
|
|
|
? t("providerForm.officialNoApiKey", {
|
|
|
|
|
|
defaultValue: "官方供应商无需 API Key",
|
|
|
|
|
|
})
|
|
|
|
|
|
: t("providerForm.apiKeyAutoFill", {
|
|
|
|
|
|
defaultValue: "输入 API Key,将自动填充到配置",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
disabled={category === "official"}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* API Key 获取链接 */}
|
|
|
|
|
|
{shouldShowClaudeApiKeyLink && claudeWebsiteUrl && (
|
|
|
|
|
|
<div className="-mt-1 pl-1">
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={claudeWebsiteUrl}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
|
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
{t("providerForm.getApiKey", {
|
|
|
|
|
|
defaultValue: "获取 API Key",
|
|
|
|
|
|
})}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-16 17:40:25 +08:00
|
|
|
|
|
2025-10-16 20:25:39 +08:00
|
|
|
|
{/* 模板变量输入(仅 Claude 且有模板变量时显示) */}
|
|
|
|
|
|
{appType === "claude" && templateValueEntries.length > 0 && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<FormLabel>
|
|
|
|
|
|
{t("providerForm.parameterConfig", {
|
|
|
|
|
|
name: templatePreset?.name || "",
|
|
|
|
|
|
defaultValue: `${templatePreset?.name || ""} 参数配置`,
|
|
|
|
|
|
})}
|
|
|
|
|
|
</FormLabel>
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{templateValueEntries.map(([key, config]) => (
|
|
|
|
|
|
<div key={key} className="space-y-2">
|
|
|
|
|
|
<FormLabel htmlFor={`template-${key}`}>
|
|
|
|
|
|
{config.label}
|
|
|
|
|
|
</FormLabel>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id={`template-${key}`}
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
required
|
|
|
|
|
|
value={
|
|
|
|
|
|
templateValues[key]?.editorValue ??
|
|
|
|
|
|
config.editorValue ??
|
|
|
|
|
|
config.defaultValue ??
|
|
|
|
|
|
""
|
|
|
|
|
|
}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleTemplateValueChange(key, e.target.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder={config.placeholder || config.label}
|
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-16 17:44:23 +08:00
|
|
|
|
{/* Base URL 输入框(仅 Claude 第三方/自定义显示) */}
|
|
|
|
|
|
{appType === "claude" && shouldShowSpeedTest && (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<FormLabel htmlFor="baseUrl">
|
|
|
|
|
|
{t("providerForm.apiEndpoint", { defaultValue: "API 端点" })}
|
|
|
|
|
|
</FormLabel>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setIsEndpointModalOpen(true)}
|
|
|
|
|
|
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Zap className="h-3.5 w-3.5" />
|
2025-10-16 20:21:42 +08:00
|
|
|
|
{t("providerForm.manageAndTest", {
|
|
|
|
|
|
defaultValue: "管理和测速",
|
|
|
|
|
|
})}
|
2025-10-16 17:44:23 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="baseUrl"
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
value={baseUrl}
|
|
|
|
|
|
onChange={(e) => handleClaudeBaseUrlChange(e.target.value)}
|
2025-10-16 20:21:42 +08:00
|
|
|
|
placeholder={t("providerForm.apiEndpointPlaceholder", {
|
|
|
|
|
|
defaultValue: "https://api.example.com",
|
|
|
|
|
|
})}
|
2025-10-16 17:44:23 +08:00
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
|
|
|
|
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
2025-10-16 20:21:42 +08:00
|
|
|
|
{t("providerForm.apiHint", {
|
|
|
|
|
|
defaultValue: "API 端点地址用于连接服务器",
|
|
|
|
|
|
})}
|
2025-10-16 17:44:23 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 端点测速弹窗 - Claude */}
|
|
|
|
|
|
{appType === "claude" && shouldShowSpeedTest && isEndpointModalOpen && (
|
|
|
|
|
|
<EndpointSpeedTest
|
|
|
|
|
|
appType={appType}
|
|
|
|
|
|
value={baseUrl}
|
|
|
|
|
|
onChange={handleClaudeBaseUrlChange}
|
|
|
|
|
|
initialEndpoints={[{ url: baseUrl }]}
|
|
|
|
|
|
visible={isEndpointModalOpen}
|
|
|
|
|
|
onClose={() => setIsEndpointModalOpen(false)}
|
2025-10-16 19:40:22 +08:00
|
|
|
|
onCustomEndpointsChange={setDraftCustomEndpoints}
|
2025-10-16 17:44:23 +08:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-16 20:21:42 +08:00
|
|
|
|
{/* 模型选择器(仅 Claude 非官方且非 Kimi 供应商显示) */}
|
|
|
|
|
|
{appType === "claude" &&
|
|
|
|
|
|
category !== "official" &&
|
|
|
|
|
|
!shouldShowKimiSelector && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
{/* ANTHROPIC_MODEL */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<FormLabel htmlFor="claudeModel">
|
|
|
|
|
|
{t("providerForm.anthropicModel", {
|
|
|
|
|
|
defaultValue: "主模型",
|
|
|
|
|
|
})}
|
|
|
|
|
|
</FormLabel>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="claudeModel"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={claudeModel}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleModelChange("ANTHROPIC_MODEL", e.target.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder={t("providerForm.modelPlaceholder", {
|
|
|
|
|
|
defaultValue: "claude-3-7-sonnet-20250219",
|
|
|
|
|
|
})}
|
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ANTHROPIC_SMALL_FAST_MODEL */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<FormLabel htmlFor="claudeSmallFastModel">
|
|
|
|
|
|
{t("providerForm.anthropicSmallFastModel", {
|
|
|
|
|
|
defaultValue: "快速模型",
|
|
|
|
|
|
})}
|
|
|
|
|
|
</FormLabel>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="claudeSmallFastModel"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={claudeSmallFastModel}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleModelChange(
|
|
|
|
|
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
|
|
|
|
e.target.value
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder={t("providerForm.smallModelPlaceholder", {
|
|
|
|
|
|
defaultValue: "claude-3-5-haiku-20241022",
|
|
|
|
|
|
})}
|
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-10-16 17:58:49 +08:00
|
|
|
|
</div>
|
2025-10-16 20:21:42 +08:00
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
{t("providerForm.modelHelper", {
|
|
|
|
|
|
defaultValue:
|
|
|
|
|
|
"可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
|
|
|
|
|
|
})}
|
|
|
|
|
|
</p>
|
2025-10-16 17:58:49 +08:00
|
|
|
|
</div>
|
2025-10-16 20:21:42 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Kimi 模型选择器(仅 Claude 且是 Kimi 供应商时显示) */}
|
|
|
|
|
|
{appType === "claude" && shouldShowKimiSelector && (
|
|
|
|
|
|
<KimiModelSelector
|
|
|
|
|
|
apiKey={apiKey}
|
|
|
|
|
|
anthropicModel={kimiAnthropicModel}
|
|
|
|
|
|
anthropicSmallFastModel={kimiAnthropicSmallFastModel}
|
|
|
|
|
|
onModelChange={handleKimiModelChange}
|
|
|
|
|
|
disabled={category === "official"}
|
|
|
|
|
|
/>
|
2025-10-16 17:58:49 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-16 18:50:44 +08:00
|
|
|
|
{/* Codex API Key 输入框 */}
|
|
|
|
|
|
{appType === "codex" && !isEditMode && (
|
2025-10-16 19:37:43 +08:00
|
|
|
|
<div className="space-y-1">
|
2025-10-16 18:50:44 +08:00
|
|
|
|
<ApiKeyInput
|
|
|
|
|
|
id="codexApiKey"
|
|
|
|
|
|
label="API Key"
|
|
|
|
|
|
value={codexApiKey}
|
|
|
|
|
|
onChange={handleCodexApiKeyChange}
|
|
|
|
|
|
required={category !== "official"}
|
|
|
|
|
|
placeholder={
|
|
|
|
|
|
category === "official"
|
2025-10-16 20:21:42 +08:00
|
|
|
|
? t("providerForm.codexOfficialNoApiKey", {
|
|
|
|
|
|
defaultValue: "官方供应商无需 API Key",
|
|
|
|
|
|
})
|
|
|
|
|
|
: t("providerForm.codexApiKeyAutoFill", {
|
|
|
|
|
|
defaultValue: "输入 API Key,将自动填充到配置",
|
|
|
|
|
|
})
|
2025-10-16 18:50:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
disabled={category === "official"}
|
|
|
|
|
|
/>
|
2025-10-16 19:37:43 +08:00
|
|
|
|
{/* Codex API Key 获取链接 */}
|
2025-10-16 19:56:00 +08:00
|
|
|
|
{shouldShowCodexApiKeyLink && codexWebsiteUrl && (
|
2025-10-16 19:37:43 +08:00
|
|
|
|
<div className="-mt-1 pl-1">
|
|
|
|
|
|
<a
|
2025-10-16 19:56:00 +08:00
|
|
|
|
href={codexWebsiteUrl}
|
2025-10-16 19:37:43 +08:00
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
|
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
|
|
|
|
|
>
|
2025-10-16 20:21:42 +08:00
|
|
|
|
{t("providerForm.getApiKey", {
|
|
|
|
|
|
defaultValue: "获取 API Key",
|
|
|
|
|
|
})}
|
2025-10-16 19:37:43 +08:00
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-16 18:50:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Codex Base URL 输入框 */}
|
|
|
|
|
|
{appType === "codex" && shouldShowSpeedTest && (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<FormLabel htmlFor="codexBaseUrl">
|
|
|
|
|
|
{t("codexConfig.apiUrlLabel", { defaultValue: "API 端点" })}
|
2025-10-16 10:49:56 +08:00
|
|
|
|
</FormLabel>
|
2025-10-16 18:50:44 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setIsCodexEndpointModalOpen(true)}
|
|
|
|
|
|
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Zap className="h-3.5 w-3.5" />
|
2025-10-16 20:21:42 +08:00
|
|
|
|
{t("providerForm.manageAndTest", {
|
|
|
|
|
|
defaultValue: "管理和测速",
|
|
|
|
|
|
})}
|
2025-10-16 18:50:44 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="codexBaseUrl"
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
value={codexBaseUrl}
|
|
|
|
|
|
onChange={(e) => handleCodexBaseUrlChange(e.target.value)}
|
2025-10-16 20:21:42 +08:00
|
|
|
|
placeholder={t("providerForm.codexApiEndpointPlaceholder", {
|
|
|
|
|
|
defaultValue: "https://api.example.com/v1",
|
|
|
|
|
|
})}
|
2025-10-16 18:50:44 +08:00
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
|
|
|
|
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
2025-10-16 20:21:42 +08:00
|
|
|
|
{t("providerForm.codexApiHint", {
|
|
|
|
|
|
defaultValue: "Codex API 端点地址",
|
|
|
|
|
|
})}
|
2025-10-16 18:50:44 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 端点测速弹窗 - Codex */}
|
2025-10-16 20:21:42 +08:00
|
|
|
|
{appType === "codex" &&
|
|
|
|
|
|
shouldShowSpeedTest &&
|
|
|
|
|
|
isCodexEndpointModalOpen && (
|
|
|
|
|
|
<EndpointSpeedTest
|
|
|
|
|
|
appType={appType}
|
|
|
|
|
|
value={codexBaseUrl}
|
|
|
|
|
|
onChange={handleCodexBaseUrlChange}
|
|
|
|
|
|
initialEndpoints={[{ url: codexBaseUrl }]}
|
|
|
|
|
|
visible={isCodexEndpointModalOpen}
|
|
|
|
|
|
onClose={() => setIsCodexEndpointModalOpen(false)}
|
|
|
|
|
|
onCustomEndpointsChange={setDraftCustomEndpoints}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-10-16 18:50:44 +08:00
|
|
|
|
|
2025-10-16 20:32:11 +08:00
|
|
|
|
{/* 配置编辑器:Claude 使用通用配置编辑器,Codex 使用专用编辑器 */}
|
2025-10-16 18:50:44 +08:00
|
|
|
|
{appType === "codex" ? (
|
|
|
|
|
|
<CodexConfigEditor
|
|
|
|
|
|
authValue={codexAuth}
|
|
|
|
|
|
configValue={codexConfig}
|
|
|
|
|
|
onAuthChange={setCodexAuth}
|
|
|
|
|
|
onConfigChange={handleCodexConfigChange}
|
|
|
|
|
|
useCommonConfig={false}
|
|
|
|
|
|
onCommonConfigToggle={() => {}}
|
|
|
|
|
|
commonConfigSnippet=""
|
|
|
|
|
|
onCommonConfigSnippetChange={() => {}}
|
|
|
|
|
|
commonConfigError=""
|
|
|
|
|
|
authError={codexAuthError}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
2025-10-16 20:32:11 +08:00
|
|
|
|
<CommonConfigEditor
|
|
|
|
|
|
value={form.watch("settingsConfig")}
|
|
|
|
|
|
onChange={(value) => form.setValue("settingsConfig", value)}
|
|
|
|
|
|
useCommonConfig={useCommonConfig}
|
|
|
|
|
|
onCommonConfigToggle={handleCommonConfigToggle}
|
|
|
|
|
|
commonConfigSnippet={commonConfigSnippet}
|
|
|
|
|
|
onCommonConfigSnippetChange={handleCommonConfigSnippetChange}
|
|
|
|
|
|
commonConfigError={commonConfigError}
|
|
|
|
|
|
onEditClick={() => setIsCommonConfigModalOpen(true)}
|
|
|
|
|
|
isModalOpen={isCommonConfigModalOpen}
|
|
|
|
|
|
onModalClose={() => setIsCommonConfigModalOpen(false)}
|
2025-10-16 18:50:44 +08:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-10-16 10:49:56 +08:00
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-end gap-2">
|
|
|
|
|
|
<Button variant="outline" type="button" onClick={onCancel}>
|
|
|
|
|
|
{t("common.cancel", { defaultValue: "取消" })}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="submit">{submitLabel}</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 13:02:38 +08:00
|
|
|
|
export type ProviderFormValues = ProviderFormData & {
|
|
|
|
|
|
presetId?: string;
|
|
|
|
|
|
presetCategory?: ProviderCategory;
|
2025-10-16 19:40:22 +08:00
|
|
|
|
meta?: {
|
|
|
|
|
|
custom_endpoints?: Record<string, CustomEndpoint>;
|
|
|
|
|
|
};
|
2025-10-16 13:02:38 +08:00
|
|
|
|
};
|