refactor: extract API Key link and custom endpoints logic into hooks

- Create useApiKeyLink hook to manage API Key retrieval link display and URL
- Create useCustomEndpoints hook to collect endpoints from multiple sources
- Simplify ProviderForm by using these new hooks
- Reduce code duplication and improve maintainability
- Fix TypeScript error with form.watch("websiteUrl") by providing default empty string
This commit is contained in:
Jason
2025-10-16 19:56:00 +08:00
parent fe4b3e9957
commit 6541c14421
4 changed files with 197 additions and 90 deletions

View File

@@ -33,6 +33,8 @@ import {
useBaseUrlState,
useModelState,
useCodexConfigState,
useApiKeyLink,
useCustomEndpoints,
} from "./hooks";
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
@@ -202,49 +204,9 @@ export function ProviderForm({
}
}
// 若为"新建供应商",将端点候选一并随提交落盘到 meta.custom_endpoints
// - 用户在弹窗中新增的自定义端点draftCustomEndpoints,已去重)
// - 预设中的 endpointCandidates若存在
// - 当前选中的基础 URLbaseUrl/codexBaseUrl
if (!initialData) {
const urlSet = new Set<string>();
const push = (raw?: string) => {
const url = (raw || "").trim().replace(/\/+$/, "");
if (url) urlSet.add(url);
};
// 自定义端点(仅来自用户新增)
for (const u of draftCustomEndpoints) push(u);
// 预设端点候选
if (selectedPresetId && selectedPresetId !== "custom") {
const entry = presetEntries.find((item) => item.id === selectedPresetId);
if (entry) {
const preset = entry.preset as any;
if (Array.isArray(preset?.endpointCandidates)) {
for (const u of preset.endpointCandidates as string[]) push(u);
}
}
}
// 当前 Base URL
if (appType === "codex") {
push(codexBaseUrl);
} else {
push(baseUrl);
}
const urls = Array.from(urlSet.values());
if (urls.length > 0) {
const now = Date.now();
const customMap: Record<string, CustomEndpoint> = {};
for (const url of urls) {
if (!customMap[url]) {
customMap[url] = { url, addedAt: now, lastUsed: undefined };
}
}
payload.meta = { custom_endpoints: customMap };
}
// 新建供应商时:添加自定义端点
if (!initialData && customEndpointsMap) {
payload.meta = { custom_endpoints: customEndpointsMap };
}
onSubmit(payload);
@@ -302,51 +264,39 @@ export function ProviderForm({
const shouldShowSpeedTest =
category === "third_party" || category === "custom";
// 判断是否显示 Claude API Key 获取链接
const shouldShowClaudeApiKeyLink =
appType === "claude" &&
category !== "official" &&
(category === "cn_official" ||
category === "aggregator" ||
category === "third_party");
// 使用 API Key 链接 hook (Claude)
const {
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
websiteUrl: claudeWebsiteUrl,
} = useApiKeyLink({
appType: "claude",
category,
selectedPresetId,
presetEntries,
formWebsiteUrl: form.watch("websiteUrl") || "",
});
// 获取当前 Claude 供应商的网址(用于 API Key 链接)
const getCurrentClaudeWebsiteUrl = (): string => {
if (selectedPresetId && selectedPresetId !== "custom") {
const entry = presetEntries.find((item) => item.id === selectedPresetId);
if (entry) {
const preset = entry.preset as ProviderPreset;
// 第三方供应商优先使用 apiKeyUrl
return preset.category === "third_party"
? preset.apiKeyUrl || preset.websiteUrl || ""
: preset.websiteUrl || "";
}
}
return form.watch("websiteUrl") || "";
};
// 使用 API Key 链接 hook (Codex)
const {
shouldShowApiKeyLink: shouldShowCodexApiKeyLink,
websiteUrl: codexWebsiteUrl,
} = useApiKeyLink({
appType: "codex",
category,
selectedPresetId,
presetEntries,
formWebsiteUrl: form.watch("websiteUrl") || "",
});
// 判断是否显示 Codex API Key 获取链接
const shouldShowCodexApiKeyLink =
appType === "codex" &&
category !== "official" &&
(category === "cn_official" ||
category === "aggregator" ||
category === "third_party");
// 获取当前 Codex 供应商的网址(用于 API Key 链接)
const getCurrentCodexWebsiteUrl = (): string => {
if (selectedPresetId && selectedPresetId !== "custom") {
const entry = presetEntries.find((item) => item.id === selectedPresetId);
if (entry) {
const preset = entry.preset as CodexProviderPreset;
// 第三方供应商优先使用 apiKeyUrl
return preset.category === "third_party"
? preset.apiKeyUrl || preset.websiteUrl || ""
: preset.websiteUrl || "";
}
}
return form.watch("websiteUrl") || "";
};
// 使用自定义端点 hook
const customEndpointsMap = useCustomEndpoints({
appType,
selectedPresetId,
presetEntries,
draftCustomEndpoints,
baseUrl,
codexBaseUrl,
});
const handlePresetChange = (value: string) => {
setSelectedPresetId(value);
@@ -510,10 +460,10 @@ export function ProviderForm({
disabled={category === "official"}
/>
{/* API Key 获取链接 */}
{shouldShowClaudeApiKeyLink && getCurrentClaudeWebsiteUrl() && (
{shouldShowClaudeApiKeyLink && claudeWebsiteUrl && (
<div className="-mt-1 pl-1">
<a
href={getCurrentClaudeWebsiteUrl()}
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"
@@ -635,10 +585,10 @@ export function ProviderForm({
disabled={category === "official"}
/>
{/* Codex API Key 获取链接 */}
{shouldShowCodexApiKeyLink && getCurrentCodexWebsiteUrl() && (
{shouldShowCodexApiKeyLink && codexWebsiteUrl && (
<div className="-mt-1 pl-1">
<a
href={getCurrentCodexWebsiteUrl()}
href={codexWebsiteUrl}
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"

View File

@@ -3,3 +3,5 @@ export { useApiKeyState } from "./useApiKeyState";
export { useBaseUrlState } from "./useBaseUrlState";
export { useModelState } from "./useModelState";
export { useCodexConfigState } from "./useCodexConfigState";
export { useApiKeyLink } from "./useApiKeyLink";
export { useCustomEndpoints } from "./useCustomEndpoints";

View File

@@ -0,0 +1,63 @@
import { useMemo } from "react";
import type { AppType } from "@/lib/api";
import type { ProviderCategory } from "@/types";
import type { ProviderPreset } from "@/config/providerPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
type PresetEntry = {
id: string;
preset: ProviderPreset | CodexProviderPreset;
};
interface UseApiKeyLinkProps {
appType: AppType;
category?: ProviderCategory;
selectedPresetId: string | null;
presetEntries: PresetEntry[];
formWebsiteUrl: string;
}
/**
* 管理 API Key 获取链接的显示和 URL
*/
export function useApiKeyLink({
appType,
category,
selectedPresetId,
presetEntries,
formWebsiteUrl,
}: UseApiKeyLinkProps) {
// 判断是否显示 API Key 获取链接
const shouldShowApiKeyLink = useMemo(() => {
return (
category !== "official" &&
(category === "cn_official" ||
category === "aggregator" ||
category === "third_party")
);
}, [category]);
// 获取当前供应商的网址(用于 API Key 链接)
const getWebsiteUrl = useMemo(() => {
if (selectedPresetId && selectedPresetId !== "custom") {
const entry = presetEntries.find((item) => item.id === selectedPresetId);
if (entry) {
const preset = entry.preset;
// 第三方供应商优先使用 apiKeyUrl
return preset.category === "third_party"
? preset.apiKeyUrl || preset.websiteUrl || ""
: preset.websiteUrl || "";
}
}
return formWebsiteUrl || "";
}, [selectedPresetId, presetEntries, formWebsiteUrl]);
return {
shouldShowApiKeyLink: appType === "claude"
? shouldShowApiKeyLink
: appType === "codex"
? shouldShowApiKeyLink
: false,
websiteUrl: getWebsiteUrl,
};
}

View File

@@ -0,0 +1,92 @@
import { useMemo } from "react";
import type { AppType } from "@/lib/api";
import type { CustomEndpoint } from "@/types";
import type { ProviderPreset } from "@/config/providerPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
type PresetEntry = {
id: string;
preset: ProviderPreset | CodexProviderPreset;
};
interface UseCustomEndpointsProps {
appType: AppType;
selectedPresetId: string | null;
presetEntries: PresetEntry[];
draftCustomEndpoints: string[];
baseUrl: string;
codexBaseUrl: string;
}
/**
* 收集和管理自定义端点
*
* 收集来源:
* 1. 用户在测速弹窗中新增的自定义端点
* 2. 预设中的 endpointCandidates
* 3. 当前选中的 Base URL
*/
export function useCustomEndpoints({
appType,
selectedPresetId,
presetEntries,
draftCustomEndpoints,
baseUrl,
codexBaseUrl,
}: UseCustomEndpointsProps) {
const customEndpointsMap = useMemo(() => {
const urlSet = new Set<string>();
// 辅助函数:标准化并添加 URL
const push = (raw?: string) => {
const url = (raw || "").trim().replace(/\/+$/, "");
if (url) urlSet.add(url);
};
// 1. 自定义端点(来自用户新增)
for (const u of draftCustomEndpoints) push(u);
// 2. 预设端点候选
if (selectedPresetId && selectedPresetId !== "custom") {
const entry = presetEntries.find((item) => item.id === selectedPresetId);
if (entry) {
const preset = entry.preset as any;
if (Array.isArray(preset?.endpointCandidates)) {
for (const u of preset.endpointCandidates as string[]) push(u);
}
}
}
// 3. 当前 Base URL
if (appType === "codex") {
push(codexBaseUrl);
} else {
push(baseUrl);
}
// 构建 CustomEndpoint map
const urls = Array.from(urlSet.values());
if (urls.length === 0) {
return null;
}
const now = Date.now();
const customMap: Record<string, CustomEndpoint> = {};
for (const url of urls) {
if (!customMap[url]) {
customMap[url] = { url, addedAt: now, lastUsed: undefined };
}
}
return customMap;
}, [
appType,
selectedPresetId,
presetEntries,
draftCustomEndpoints,
baseUrl,
codexBaseUrl,
]);
return customEndpointsMap;
}