refactor: extract Kimi model selector logic to dedicated hook
- Create useKimiModelSelector hook to manage Kimi-specific state - Auto-detect Kimi providers by preset name or config content - Support model initialization from existing config in edit mode - Sync model selections with JSON configuration - Maintain clean separation between UI and business logic
This commit is contained in:
@@ -26,6 +26,7 @@ import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
|||||||
import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput";
|
import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput";
|
||||||
import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest";
|
import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest";
|
||||||
import CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor";
|
import CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor";
|
||||||
|
import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector";
|
||||||
import { Zap } from "lucide-react";
|
import { Zap } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
useProviderCategory,
|
useProviderCategory,
|
||||||
@@ -35,6 +36,7 @@ import {
|
|||||||
useCodexConfigState,
|
useCodexConfigState,
|
||||||
useApiKeyLink,
|
useApiKeyLink,
|
||||||
useCustomEndpoints,
|
useCustomEndpoints,
|
||||||
|
useKimiModelSelector,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
|
|
||||||
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
|
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
|
||||||
@@ -69,7 +71,7 @@ export function ProviderForm({
|
|||||||
const isEditMode = Boolean(initialData);
|
const isEditMode = Boolean(initialData);
|
||||||
|
|
||||||
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(
|
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(
|
||||||
initialData ? null : "custom",
|
initialData ? null : "custom"
|
||||||
);
|
);
|
||||||
const [activePreset, setActivePreset] = useState<{
|
const [activePreset, setActivePreset] = useState<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -78,7 +80,9 @@ export function ProviderForm({
|
|||||||
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
||||||
|
|
||||||
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
|
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
|
||||||
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>([]);
|
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// 使用 category hook
|
// 使用 category hook
|
||||||
const { category } = useProviderCategory({
|
const { category } = useProviderCategory({
|
||||||
@@ -102,7 +106,7 @@ export function ProviderForm({
|
|||||||
? CODEX_DEFAULT_CONFIG
|
? CODEX_DEFAULT_CONFIG
|
||||||
: CLAUDE_DEFAULT_CONFIG,
|
: CLAUDE_DEFAULT_CONFIG,
|
||||||
}),
|
}),
|
||||||
[initialData, appType],
|
[initialData, appType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = useForm<ProviderFormData>({
|
const form = useForm<ProviderFormData>({
|
||||||
@@ -112,7 +116,11 @@ export function ProviderForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 使用 API Key hook
|
// 使用 API Key hook
|
||||||
const { apiKey, handleApiKeyChange, showApiKey: shouldShowApiKey } = useApiKeyState({
|
const {
|
||||||
|
apiKey,
|
||||||
|
handleApiKeyChange,
|
||||||
|
showApiKey: shouldShowApiKey,
|
||||||
|
} = useApiKeyState({
|
||||||
initialConfig: form.watch("settingsConfig"),
|
initialConfig: form.watch("settingsConfig"),
|
||||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
@@ -136,10 +144,11 @@ export function ProviderForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 使用 Model hook
|
// 使用 Model hook
|
||||||
const { claudeModel, claudeSmallFastModel, handleModelChange } = useModelState({
|
const { claudeModel, claudeSmallFastModel, handleModelChange } =
|
||||||
settingsConfig: form.watch("settingsConfig"),
|
useModelState({
|
||||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
settingsConfig: form.watch("settingsConfig"),
|
||||||
});
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||||
|
});
|
||||||
|
|
||||||
// 使用 Codex 配置 hook (仅 Codex 模式)
|
// 使用 Codex 配置 hook (仅 Codex 模式)
|
||||||
const {
|
const {
|
||||||
@@ -155,7 +164,8 @@ export function ProviderForm({
|
|||||||
resetCodexConfig,
|
resetCodexConfig,
|
||||||
} = useCodexConfigState({ initialData });
|
} = useCodexConfigState({ initialData });
|
||||||
|
|
||||||
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false);
|
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset(defaultValues);
|
form.reset(defaultValues);
|
||||||
@@ -169,6 +179,55 @@ export function ProviderForm({
|
|||||||
: false;
|
: false;
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
|
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 || ""
|
||||||
|
: "",
|
||||||
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: ProviderFormData) => {
|
const handleSubmit = (values: ProviderFormData) => {
|
||||||
let settingsConfig: string;
|
let settingsConfig: string;
|
||||||
|
|
||||||
@@ -212,37 +271,6 @@ export function ProviderForm({
|
|||||||
onSubmit(payload);
|
onSubmit(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const groupedPresets = useMemo(() => {
|
const groupedPresets = useMemo(() => {
|
||||||
return presetEntries.reduce<Record<string, PresetEntry[]>>((acc, entry) => {
|
return presetEntries.reduce<Record<string, PresetEntry[]>>((acc, entry) => {
|
||||||
const category = entry.preset.category ?? "others";
|
const category = entry.preset.category ?? "others";
|
||||||
@@ -256,7 +284,7 @@ export function ProviderForm({
|
|||||||
|
|
||||||
const categoryKeys = useMemo(() => {
|
const categoryKeys = useMemo(() => {
|
||||||
return Object.keys(groupedPresets).filter(
|
return Object.keys(groupedPresets).filter(
|
||||||
(key) => key !== "custom" && groupedPresets[key]?.length,
|
(key) => key !== "custom" && groupedPresets[key]?.length
|
||||||
);
|
);
|
||||||
}, [groupedPresets]);
|
}, [groupedPresets]);
|
||||||
|
|
||||||
@@ -341,7 +369,7 @@ export function ProviderForm({
|
|||||||
const preset = entry.preset as ProviderPreset;
|
const preset = entry.preset as ProviderPreset;
|
||||||
const config = applyTemplateValues(
|
const config = applyTemplateValues(
|
||||||
preset.settingsConfig,
|
preset.settingsConfig,
|
||||||
preset.templateValues,
|
preset.templateValues
|
||||||
);
|
);
|
||||||
|
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -446,34 +474,41 @@ export function ProviderForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* API Key 输入框(仅 Claude 且非编辑模式显示) */}
|
{/* API Key 输入框(仅 Claude 且非编辑模式显示) */}
|
||||||
{appType === "claude" && shouldShowApiKey(form.watch("settingsConfig"), isEditMode) && (
|
{appType === "claude" &&
|
||||||
<div className="space-y-1">
|
shouldShowApiKey(form.watch("settingsConfig"), isEditMode) && (
|
||||||
<ApiKeyInput
|
<div className="space-y-1">
|
||||||
value={apiKey}
|
<ApiKeyInput
|
||||||
onChange={handleApiKeyChange}
|
value={apiKey}
|
||||||
required={category !== "official"}
|
onChange={handleApiKeyChange}
|
||||||
placeholder={
|
required={category !== "official"}
|
||||||
category === "official"
|
placeholder={
|
||||||
? t("providerForm.officialNoApiKey", { defaultValue: "官方供应商无需 API Key" })
|
category === "official"
|
||||||
: t("providerForm.apiKeyAutoFill", { defaultValue: "输入 API Key,将自动填充到配置" })
|
? t("providerForm.officialNoApiKey", {
|
||||||
}
|
defaultValue: "官方供应商无需 API Key",
|
||||||
disabled={category === "official"}
|
})
|
||||||
/>
|
: t("providerForm.apiKeyAutoFill", {
|
||||||
{/* API Key 获取链接 */}
|
defaultValue: "输入 API Key,将自动填充到配置",
|
||||||
{shouldShowClaudeApiKeyLink && claudeWebsiteUrl && (
|
})
|
||||||
<div className="-mt-1 pl-1">
|
}
|
||||||
<a
|
disabled={category === "official"}
|
||||||
href={claudeWebsiteUrl}
|
/>
|
||||||
target="_blank"
|
{/* API Key 获取链接 */}
|
||||||
rel="noopener noreferrer"
|
{shouldShowClaudeApiKeyLink && claudeWebsiteUrl && (
|
||||||
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
<div className="-mt-1 pl-1">
|
||||||
>
|
<a
|
||||||
{t("providerForm.getApiKey", { defaultValue: "获取 API Key" })}
|
href={claudeWebsiteUrl}
|
||||||
</a>
|
target="_blank"
|
||||||
</div>
|
rel="noopener noreferrer"
|
||||||
)}
|
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||||
</div>
|
>
|
||||||
)}
|
{t("providerForm.getApiKey", {
|
||||||
|
defaultValue: "获取 API Key",
|
||||||
|
})}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Base URL 输入框(仅 Claude 第三方/自定义显示) */}
|
{/* Base URL 输入框(仅 Claude 第三方/自定义显示) */}
|
||||||
{appType === "claude" && shouldShowSpeedTest && (
|
{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"
|
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" />
|
<Zap className="h-3.5 w-3.5" />
|
||||||
{t("providerForm.manageAndTest", { defaultValue: "管理和测速" })}
|
{t("providerForm.manageAndTest", {
|
||||||
|
defaultValue: "管理和测速",
|
||||||
|
})}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@@ -496,12 +533,16 @@ export function ProviderForm({
|
|||||||
type="url"
|
type="url"
|
||||||
value={baseUrl}
|
value={baseUrl}
|
||||||
onChange={(e) => handleClaudeBaseUrlChange(e.target.value)}
|
onChange={(e) => handleClaudeBaseUrlChange(e.target.value)}
|
||||||
placeholder={t("providerForm.apiEndpointPlaceholder", { defaultValue: "https://api.example.com" })}
|
placeholder={t("providerForm.apiEndpointPlaceholder", {
|
||||||
|
defaultValue: "https://api.example.com",
|
||||||
|
})}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
<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">
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
{t("providerForm.apiHint", { defaultValue: "API 端点地址用于连接服务器" })}
|
{t("providerForm.apiHint", {
|
||||||
|
defaultValue: "API 端点地址用于连接服务器",
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -520,52 +561,75 @@ export function ProviderForm({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 模型选择器(仅 Claude 非官方供应商显示) */}
|
{/* 模型选择器(仅 Claude 非官方且非 Kimi 供应商显示) */}
|
||||||
{appType === "claude" && category !== "official" && (
|
{appType === "claude" &&
|
||||||
<div className="space-y-3">
|
category !== "official" &&
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
!shouldShowKimiSelector && (
|
||||||
{/* ANTHROPIC_MODEL */}
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormLabel htmlFor="claudeModel">
|
{/* ANTHROPIC_MODEL */}
|
||||||
{t("providerForm.anthropicModel", { defaultValue: "主模型" })}
|
<div className="space-y-2">
|
||||||
</FormLabel>
|
<FormLabel htmlFor="claudeModel">
|
||||||
<Input
|
{t("providerForm.anthropicModel", {
|
||||||
id="claudeModel"
|
defaultValue: "主模型",
|
||||||
type="text"
|
})}
|
||||||
value={claudeModel}
|
</FormLabel>
|
||||||
onChange={(e) => handleModelChange("ANTHROPIC_MODEL", e.target.value)}
|
<Input
|
||||||
placeholder={t("providerForm.modelPlaceholder", {
|
id="claudeModel"
|
||||||
defaultValue: "claude-3-7-sonnet-20250219"
|
type="text"
|
||||||
})}
|
value={claudeModel}
|
||||||
autoComplete="off"
|
onChange={(e) =>
|
||||||
/>
|
handleModelChange("ANTHROPIC_MODEL", e.target.value)
|
||||||
</div>
|
}
|
||||||
|
placeholder={t("providerForm.modelPlaceholder", {
|
||||||
|
defaultValue: "claude-3-7-sonnet-20250219",
|
||||||
|
})}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ANTHROPIC_SMALL_FAST_MODEL */}
|
{/* ANTHROPIC_SMALL_FAST_MODEL */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<FormLabel htmlFor="claudeSmallFastModel">
|
<FormLabel htmlFor="claudeSmallFastModel">
|
||||||
{t("providerForm.anthropicSmallFastModel", {
|
{t("providerForm.anthropicSmallFastModel", {
|
||||||
defaultValue: "快速模型"
|
defaultValue: "快速模型",
|
||||||
})}
|
})}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
id="claudeSmallFastModel"
|
id="claudeSmallFastModel"
|
||||||
type="text"
|
type="text"
|
||||||
value={claudeSmallFastModel}
|
value={claudeSmallFastModel}
|
||||||
onChange={(e) => handleModelChange("ANTHROPIC_SMALL_FAST_MODEL", e.target.value)}
|
onChange={(e) =>
|
||||||
placeholder={t("providerForm.smallModelPlaceholder", {
|
handleModelChange(
|
||||||
defaultValue: "claude-3-5-haiku-20241022"
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
})}
|
e.target.value
|
||||||
autoComplete="off"
|
)
|
||||||
/>
|
}
|
||||||
|
placeholder={t("providerForm.smallModelPlaceholder", {
|
||||||
|
defaultValue: "claude-3-5-haiku-20241022",
|
||||||
|
})}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("providerForm.modelHelper", {
|
||||||
|
defaultValue:
|
||||||
|
"可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
)}
|
||||||
{t("providerForm.modelHelper", {
|
|
||||||
defaultValue: "可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
|
{/* Kimi 模型选择器(仅 Claude 且是 Kimi 供应商时显示) */}
|
||||||
})}
|
{appType === "claude" && shouldShowKimiSelector && (
|
||||||
</p>
|
<KimiModelSelector
|
||||||
</div>
|
apiKey={apiKey}
|
||||||
|
anthropicModel={kimiAnthropicModel}
|
||||||
|
anthropicSmallFastModel={kimiAnthropicSmallFastModel}
|
||||||
|
onModelChange={handleKimiModelChange}
|
||||||
|
disabled={category === "official"}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Codex API Key 输入框 */}
|
{/* Codex API Key 输入框 */}
|
||||||
@@ -579,8 +643,12 @@ export function ProviderForm({
|
|||||||
required={category !== "official"}
|
required={category !== "official"}
|
||||||
placeholder={
|
placeholder={
|
||||||
category === "official"
|
category === "official"
|
||||||
? t("providerForm.codexOfficialNoApiKey", { defaultValue: "官方供应商无需 API Key" })
|
? t("providerForm.codexOfficialNoApiKey", {
|
||||||
: t("providerForm.codexApiKeyAutoFill", { defaultValue: "输入 API Key,将自动填充到配置" })
|
defaultValue: "官方供应商无需 API Key",
|
||||||
|
})
|
||||||
|
: t("providerForm.codexApiKeyAutoFill", {
|
||||||
|
defaultValue: "输入 API Key,将自动填充到配置",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
disabled={category === "official"}
|
disabled={category === "official"}
|
||||||
/>
|
/>
|
||||||
@@ -593,7 +661,9 @@ export function ProviderForm({
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
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",
|
||||||
|
})}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -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"
|
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" />
|
<Zap className="h-3.5 w-3.5" />
|
||||||
{t("providerForm.manageAndTest", { defaultValue: "管理和测速" })}
|
{t("providerForm.manageAndTest", {
|
||||||
|
defaultValue: "管理和测速",
|
||||||
|
})}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@@ -621,29 +693,35 @@ export function ProviderForm({
|
|||||||
type="url"
|
type="url"
|
||||||
value={codexBaseUrl}
|
value={codexBaseUrl}
|
||||||
onChange={(e) => handleCodexBaseUrlChange(e.target.value)}
|
onChange={(e) => 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"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
<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">
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
{t("providerForm.codexApiHint", { defaultValue: "Codex API 端点地址" })}
|
{t("providerForm.codexApiHint", {
|
||||||
|
defaultValue: "Codex API 端点地址",
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 端点测速弹窗 - Codex */}
|
{/* 端点测速弹窗 - Codex */}
|
||||||
{appType === "codex" && shouldShowSpeedTest && isCodexEndpointModalOpen && (
|
{appType === "codex" &&
|
||||||
<EndpointSpeedTest
|
shouldShowSpeedTest &&
|
||||||
appType={appType}
|
isCodexEndpointModalOpen && (
|
||||||
value={codexBaseUrl}
|
<EndpointSpeedTest
|
||||||
onChange={handleCodexBaseUrlChange}
|
appType={appType}
|
||||||
initialEndpoints={[{ url: codexBaseUrl }]}
|
value={codexBaseUrl}
|
||||||
visible={isCodexEndpointModalOpen}
|
onChange={handleCodexBaseUrlChange}
|
||||||
onClose={() => setIsCodexEndpointModalOpen(false)}
|
initialEndpoints={[{ url: codexBaseUrl }]}
|
||||||
onCustomEndpointsChange={setDraftCustomEndpoints}
|
visible={isCodexEndpointModalOpen}
|
||||||
/>
|
onClose={() => setIsCodexEndpointModalOpen(false)}
|
||||||
)}
|
onCustomEndpointsChange={setDraftCustomEndpoints}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 配置编辑器:Claude 使用 JSON 编辑器,Codex 使用专用编辑器 */}
|
{/* 配置编辑器:Claude 使用 JSON 编辑器,Codex 使用专用编辑器 */}
|
||||||
{appType === "codex" ? (
|
{appType === "codex" ? (
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export { useModelState } from "./useModelState";
|
|||||||
export { useCodexConfigState } from "./useCodexConfigState";
|
export { useCodexConfigState } from "./useCodexConfigState";
|
||||||
export { useApiKeyLink } from "./useApiKeyLink";
|
export { useApiKeyLink } from "./useApiKeyLink";
|
||||||
export { useCustomEndpoints } from "./useCustomEndpoints";
|
export { useCustomEndpoints } from "./useCustomEndpoints";
|
||||||
|
export { useKimiModelSelector } from "./useKimiModelSelector";
|
||||||
|
|||||||
105
src/components/providers/forms/hooks/useKimiModelSelector.ts
Normal file
105
src/components/providers/forms/hooks/useKimiModelSelector.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
interface UseKimiModelSelectorProps {
|
||||||
|
initialData?: {
|
||||||
|
settingsConfig?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
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<string, unknown> };
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user