diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index ff0df78..5d7ffc9 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -26,6 +26,7 @@ import { applyTemplateValues } from "@/utils/providerConfigUtils"; import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput"; import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest"; import CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor"; +import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector"; import { Zap } from "lucide-react"; import { useProviderCategory, @@ -35,6 +36,7 @@ import { useCodexConfigState, useApiKeyLink, useCustomEndpoints, + useKimiModelSelector, } from "./hooks"; const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2); @@ -69,7 +71,7 @@ export function ProviderForm({ const isEditMode = Boolean(initialData); const [selectedPresetId, setSelectedPresetId] = useState( - initialData ? null : "custom", + initialData ? null : "custom" ); const [activePreset, setActivePreset] = useState<{ id: string; @@ -78,7 +80,9 @@ export function ProviderForm({ const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); // 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints - const [draftCustomEndpoints, setDraftCustomEndpoints] = useState([]); + const [draftCustomEndpoints, setDraftCustomEndpoints] = useState( + [] + ); // 使用 category hook const { category } = useProviderCategory({ @@ -102,7 +106,7 @@ export function ProviderForm({ ? CODEX_DEFAULT_CONFIG : CLAUDE_DEFAULT_CONFIG, }), - [initialData, appType], + [initialData, appType] ); const form = useForm({ @@ -112,7 +116,11 @@ export function ProviderForm({ }); // 使用 API Key hook - const { apiKey, handleApiKeyChange, showApiKey: shouldShowApiKey } = useApiKeyState({ + const { + apiKey, + handleApiKeyChange, + showApiKey: shouldShowApiKey, + } = useApiKeyState({ initialConfig: form.watch("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), selectedPresetId, @@ -136,10 +144,11 @@ export function ProviderForm({ }); // 使用 Model hook - const { claudeModel, claudeSmallFastModel, handleModelChange } = useModelState({ - settingsConfig: form.watch("settingsConfig"), - onConfigChange: (config) => form.setValue("settingsConfig", config), - }); + const { claudeModel, claudeSmallFastModel, handleModelChange } = + useModelState({ + settingsConfig: form.watch("settingsConfig"), + onConfigChange: (config) => form.setValue("settingsConfig", config), + }); // 使用 Codex 配置 hook (仅 Codex 模式) const { @@ -155,7 +164,8 @@ export function ProviderForm({ resetCodexConfig, } = useCodexConfigState({ initialData }); - const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false); + const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = + useState(false); useEffect(() => { form.reset(defaultValues); @@ -169,6 +179,55 @@ export function ProviderForm({ : false; }, [theme]); + const presetCategoryLabels: Record = 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((preset, index) => ({ + id: `codex-${index}`, + preset, + })); + } + return providerPresets.map((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 || "" + : "", + }); + const handleSubmit = (values: ProviderFormData) => { let settingsConfig: string; @@ -212,37 +271,6 @@ export function ProviderForm({ onSubmit(payload); }; - const presetCategoryLabels: Record = 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((preset, index) => ({ - id: `codex-${index}`, - preset, - })); - } - return providerPresets.map((preset, index) => ({ - id: `claude-${index}`, - preset, - })); - }, [appType]); - const groupedPresets = useMemo(() => { return presetEntries.reduce>((acc, entry) => { const category = entry.preset.category ?? "others"; @@ -256,7 +284,7 @@ export function ProviderForm({ const categoryKeys = useMemo(() => { return Object.keys(groupedPresets).filter( - (key) => key !== "custom" && groupedPresets[key]?.length, + (key) => key !== "custom" && groupedPresets[key]?.length ); }, [groupedPresets]); @@ -341,7 +369,7 @@ export function ProviderForm({ const preset = entry.preset as ProviderPreset; const config = applyTemplateValues( preset.settingsConfig, - preset.templateValues, + preset.templateValues ); form.reset({ @@ -446,34 +474,41 @@ export function ProviderForm({ /> {/* API Key 输入框(仅 Claude 且非编辑模式显示) */} - {appType === "claude" && shouldShowApiKey(form.watch("settingsConfig"), isEditMode) && ( -
- - {/* API Key 获取链接 */} - {shouldShowClaudeApiKeyLink && claudeWebsiteUrl && ( - - )} -
- )} + {appType === "claude" && + shouldShowApiKey(form.watch("settingsConfig"), isEditMode) && ( +
+ + {/* API Key 获取链接 */} + {shouldShowClaudeApiKeyLink && claudeWebsiteUrl && ( + + )} +
+ )} {/* Base URL 输入框(仅 Claude 第三方/自定义显示) */} {appType === "claude" && shouldShowSpeedTest && ( @@ -488,7 +523,9 @@ export function ProviderForm({ 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" > - {t("providerForm.manageAndTest", { defaultValue: "管理和测速" })} + {t("providerForm.manageAndTest", { + defaultValue: "管理和测速", + })} handleClaudeBaseUrlChange(e.target.value)} - placeholder={t("providerForm.apiEndpointPlaceholder", { defaultValue: "https://api.example.com" })} + placeholder={t("providerForm.apiEndpointPlaceholder", { + defaultValue: "https://api.example.com", + })} autoComplete="off" />

- {t("providerForm.apiHint", { defaultValue: "API 端点地址用于连接服务器" })} + {t("providerForm.apiHint", { + defaultValue: "API 端点地址用于连接服务器", + })}

@@ -520,52 +561,75 @@ export function ProviderForm({ /> )} - {/* 模型选择器(仅 Claude 非官方供应商显示) */} - {appType === "claude" && category !== "official" && ( -
-
- {/* ANTHROPIC_MODEL */} -
- - {t("providerForm.anthropicModel", { defaultValue: "主模型" })} - - handleModelChange("ANTHROPIC_MODEL", e.target.value)} - placeholder={t("providerForm.modelPlaceholder", { - defaultValue: "claude-3-7-sonnet-20250219" - })} - autoComplete="off" - /> -
+ {/* 模型选择器(仅 Claude 非官方且非 Kimi 供应商显示) */} + {appType === "claude" && + category !== "official" && + !shouldShowKimiSelector && ( +
+
+ {/* ANTHROPIC_MODEL */} +
+ + {t("providerForm.anthropicModel", { + defaultValue: "主模型", + })} + + + handleModelChange("ANTHROPIC_MODEL", e.target.value) + } + placeholder={t("providerForm.modelPlaceholder", { + defaultValue: "claude-3-7-sonnet-20250219", + })} + autoComplete="off" + /> +
- {/* ANTHROPIC_SMALL_FAST_MODEL */} -
- - {t("providerForm.anthropicSmallFastModel", { - defaultValue: "快速模型" - })} - - handleModelChange("ANTHROPIC_SMALL_FAST_MODEL", e.target.value)} - placeholder={t("providerForm.smallModelPlaceholder", { - defaultValue: "claude-3-5-haiku-20241022" - })} - autoComplete="off" - /> + {/* ANTHROPIC_SMALL_FAST_MODEL */} +
+ + {t("providerForm.anthropicSmallFastModel", { + defaultValue: "快速模型", + })} + + + handleModelChange( + "ANTHROPIC_SMALL_FAST_MODEL", + e.target.value + ) + } + placeholder={t("providerForm.smallModelPlaceholder", { + defaultValue: "claude-3-5-haiku-20241022", + })} + autoComplete="off" + /> +
+

+ {t("providerForm.modelHelper", { + defaultValue: + "可选:指定默认使用的 Claude 模型,留空则使用系统默认。", + })} +

-

- {t("providerForm.modelHelper", { - defaultValue: "可选:指定默认使用的 Claude 模型,留空则使用系统默认。", - })} -

-
+ )} + + {/* Kimi 模型选择器(仅 Claude 且是 Kimi 供应商时显示) */} + {appType === "claude" && shouldShowKimiSelector && ( + )} {/* Codex API Key 输入框 */} @@ -579,8 +643,12 @@ export function ProviderForm({ required={category !== "official"} placeholder={ category === "official" - ? t("providerForm.codexOfficialNoApiKey", { defaultValue: "官方供应商无需 API Key" }) - : t("providerForm.codexApiKeyAutoFill", { defaultValue: "输入 API Key,将自动填充到配置" }) + ? t("providerForm.codexOfficialNoApiKey", { + defaultValue: "官方供应商无需 API Key", + }) + : t("providerForm.codexApiKeyAutoFill", { + defaultValue: "输入 API Key,将自动填充到配置", + }) } disabled={category === "official"} /> @@ -593,7 +661,9 @@ export function ProviderForm({ 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" })} + {t("providerForm.getApiKey", { + defaultValue: "获取 API Key", + })}
)} @@ -613,7 +683,9 @@ export function ProviderForm({ 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" > - {t("providerForm.manageAndTest", { defaultValue: "管理和测速" })} + {t("providerForm.manageAndTest", { + defaultValue: "管理和测速", + })}
handleCodexBaseUrlChange(e.target.value)} - placeholder={t("providerForm.codexApiEndpointPlaceholder", { defaultValue: "https://api.example.com/v1" })} + placeholder={t("providerForm.codexApiEndpointPlaceholder", { + defaultValue: "https://api.example.com/v1", + })} autoComplete="off" />

- {t("providerForm.codexApiHint", { defaultValue: "Codex API 端点地址" })} + {t("providerForm.codexApiHint", { + defaultValue: "Codex API 端点地址", + })}

)} {/* 端点测速弹窗 - Codex */} - {appType === "codex" && shouldShowSpeedTest && isCodexEndpointModalOpen && ( - setIsCodexEndpointModalOpen(false)} - onCustomEndpointsChange={setDraftCustomEndpoints} - /> - )} + {appType === "codex" && + shouldShowSpeedTest && + isCodexEndpointModalOpen && ( + setIsCodexEndpointModalOpen(false)} + onCustomEndpointsChange={setDraftCustomEndpoints} + /> + )} {/* 配置编辑器:Claude 使用 JSON 编辑器,Codex 使用专用编辑器 */} {appType === "codex" ? ( diff --git a/src/components/providers/forms/hooks/index.ts b/src/components/providers/forms/hooks/index.ts index 9ec18f6..9b51276 100644 --- a/src/components/providers/forms/hooks/index.ts +++ b/src/components/providers/forms/hooks/index.ts @@ -5,3 +5,4 @@ export { useModelState } from "./useModelState"; export { useCodexConfigState } from "./useCodexConfigState"; export { useApiKeyLink } from "./useApiKeyLink"; export { useCustomEndpoints } from "./useCustomEndpoints"; +export { useKimiModelSelector } from "./useKimiModelSelector"; diff --git a/src/components/providers/forms/hooks/useKimiModelSelector.ts b/src/components/providers/forms/hooks/useKimiModelSelector.ts new file mode 100644 index 0000000..99dc722 --- /dev/null +++ b/src/components/providers/forms/hooks/useKimiModelSelector.ts @@ -0,0 +1,105 @@ +import { useState, useEffect, useCallback } from "react"; + +interface UseKimiModelSelectorProps { + initialData?: { + settingsConfig?: Record; + }; + settingsConfig: string; + onConfigChange: (config: string) => void; + selectedPresetId: string | null; + presetName?: string; +} + +/** + * 管理 Kimi 模型选择器的状态和逻辑 + */ +export function useKimiModelSelector({ + initialData, + settingsConfig, + onConfigChange, + selectedPresetId, + presetName = "", +}: UseKimiModelSelectorProps) { + const [kimiAnthropicModel, setKimiAnthropicModel] = useState(""); + const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] = useState(""); + + // 判断是否显示 Kimi 模型选择器 + const shouldShowKimiSelector = + selectedPresetId !== null && + selectedPresetId !== "custom" && + presetName.includes("Kimi"); + + // 判断是否正在编辑 Kimi 供应商 + const isEditingKimi = Boolean( + initialData && + (settingsConfig.includes("api.moonshot.cn") && + settingsConfig.includes("ANTHROPIC_MODEL")) + ); + + const shouldShow = shouldShowKimiSelector || isEditingKimi; + + // 初始化 Kimi 模型选择(编辑模式) + useEffect(() => { + if (initialData?.settingsConfig && typeof initialData.settingsConfig === "object") { + const config = initialData.settingsConfig as { env?: Record }; + if (config.env) { + const model = typeof config.env.ANTHROPIC_MODEL === "string" + ? config.env.ANTHROPIC_MODEL + : ""; + const smallFastModel = typeof config.env.ANTHROPIC_SMALL_FAST_MODEL === "string" + ? config.env.ANTHROPIC_SMALL_FAST_MODEL + : ""; + setKimiAnthropicModel(model); + setKimiAnthropicSmallFastModel(smallFastModel); + } + } + }, [initialData]); + + // 处理 Kimi 模型变化 + const handleKimiModelChange = useCallback( + (field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", value: string) => { + if (field === "ANTHROPIC_MODEL") { + setKimiAnthropicModel(value); + } else { + setKimiAnthropicSmallFastModel(value); + } + + // 更新配置 JSON + try { + const currentConfig = JSON.parse(settingsConfig || "{}"); + if (!currentConfig.env) currentConfig.env = {}; + currentConfig.env[field] = value; + + const updatedConfigString = JSON.stringify(currentConfig, null, 2); + onConfigChange(updatedConfigString); + } catch (err) { + console.error("更新 Kimi 模型配置失败:", err); + } + }, + [settingsConfig, onConfigChange], + ); + + // 当选择 Kimi 预设时,同步模型值 + useEffect(() => { + if (shouldShowKimiSelector && settingsConfig) { + try { + const config = JSON.parse(settingsConfig); + if (config.env) { + const model = config.env.ANTHROPIC_MODEL || ""; + const smallFastModel = config.env.ANTHROPIC_SMALL_FAST_MODEL || ""; + setKimiAnthropicModel(model); + setKimiAnthropicSmallFastModel(smallFastModel); + } + } catch { + // ignore + } + } + }, [shouldShowKimiSelector, settingsConfig]); + + return { + shouldShow, + kimiAnthropicModel, + kimiAnthropicSmallFastModel, + handleKimiModelChange, + }; +}