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:
@@ -33,6 +33,8 @@ import {
|
|||||||
useBaseUrlState,
|
useBaseUrlState,
|
||||||
useModelState,
|
useModelState,
|
||||||
useCodexConfigState,
|
useCodexConfigState,
|
||||||
|
useApiKeyLink,
|
||||||
|
useCustomEndpoints,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
|
|
||||||
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
|
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
|
||||||
@@ -202,49 +204,9 @@ export function ProviderForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 若为"新建供应商",将端点候选一并随提交落盘到 meta.custom_endpoints:
|
// 新建供应商时:添加自定义端点
|
||||||
// - 用户在弹窗中新增的自定义端点(draftCustomEndpoints,已去重)
|
if (!initialData && customEndpointsMap) {
|
||||||
// - 预设中的 endpointCandidates(若存在)
|
payload.meta = { custom_endpoints: customEndpointsMap };
|
||||||
// - 当前选中的基础 URL(baseUrl/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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(payload);
|
onSubmit(payload);
|
||||||
@@ -302,51 +264,39 @@ export function ProviderForm({
|
|||||||
const shouldShowSpeedTest =
|
const shouldShowSpeedTest =
|
||||||
category === "third_party" || category === "custom";
|
category === "third_party" || category === "custom";
|
||||||
|
|
||||||
// 判断是否显示 Claude API Key 获取链接
|
// 使用 API Key 链接 hook (Claude)
|
||||||
const shouldShowClaudeApiKeyLink =
|
const {
|
||||||
appType === "claude" &&
|
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
|
||||||
category !== "official" &&
|
websiteUrl: claudeWebsiteUrl,
|
||||||
(category === "cn_official" ||
|
} = useApiKeyLink({
|
||||||
category === "aggregator" ||
|
appType: "claude",
|
||||||
category === "third_party");
|
category,
|
||||||
|
selectedPresetId,
|
||||||
|
presetEntries,
|
||||||
|
formWebsiteUrl: form.watch("websiteUrl") || "",
|
||||||
|
});
|
||||||
|
|
||||||
// 获取当前 Claude 供应商的网址(用于 API Key 链接)
|
// 使用 API Key 链接 hook (Codex)
|
||||||
const getCurrentClaudeWebsiteUrl = (): string => {
|
const {
|
||||||
if (selectedPresetId && selectedPresetId !== "custom") {
|
shouldShowApiKeyLink: shouldShowCodexApiKeyLink,
|
||||||
const entry = presetEntries.find((item) => item.id === selectedPresetId);
|
websiteUrl: codexWebsiteUrl,
|
||||||
if (entry) {
|
} = useApiKeyLink({
|
||||||
const preset = entry.preset as ProviderPreset;
|
appType: "codex",
|
||||||
// 第三方供应商优先使用 apiKeyUrl
|
category,
|
||||||
return preset.category === "third_party"
|
selectedPresetId,
|
||||||
? preset.apiKeyUrl || preset.websiteUrl || ""
|
presetEntries,
|
||||||
: preset.websiteUrl || "";
|
formWebsiteUrl: form.watch("websiteUrl") || "",
|
||||||
}
|
});
|
||||||
}
|
|
||||||
return form.watch("websiteUrl") || "";
|
|
||||||
};
|
|
||||||
|
|
||||||
// 判断是否显示 Codex API Key 获取链接
|
// 使用自定义端点 hook
|
||||||
const shouldShowCodexApiKeyLink =
|
const customEndpointsMap = useCustomEndpoints({
|
||||||
appType === "codex" &&
|
appType,
|
||||||
category !== "official" &&
|
selectedPresetId,
|
||||||
(category === "cn_official" ||
|
presetEntries,
|
||||||
category === "aggregator" ||
|
draftCustomEndpoints,
|
||||||
category === "third_party");
|
baseUrl,
|
||||||
|
codexBaseUrl,
|
||||||
// 获取当前 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") || "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePresetChange = (value: string) => {
|
const handlePresetChange = (value: string) => {
|
||||||
setSelectedPresetId(value);
|
setSelectedPresetId(value);
|
||||||
@@ -510,10 +460,10 @@ export function ProviderForm({
|
|||||||
disabled={category === "official"}
|
disabled={category === "official"}
|
||||||
/>
|
/>
|
||||||
{/* API Key 获取链接 */}
|
{/* API Key 获取链接 */}
|
||||||
{shouldShowClaudeApiKeyLink && getCurrentClaudeWebsiteUrl() && (
|
{shouldShowClaudeApiKeyLink && claudeWebsiteUrl && (
|
||||||
<div className="-mt-1 pl-1">
|
<div className="-mt-1 pl-1">
|
||||||
<a
|
<a
|
||||||
href={getCurrentClaudeWebsiteUrl()}
|
href={claudeWebsiteUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
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"
|
||||||
@@ -635,10 +585,10 @@ export function ProviderForm({
|
|||||||
disabled={category === "official"}
|
disabled={category === "official"}
|
||||||
/>
|
/>
|
||||||
{/* Codex API Key 获取链接 */}
|
{/* Codex API Key 获取链接 */}
|
||||||
{shouldShowCodexApiKeyLink && getCurrentCodexWebsiteUrl() && (
|
{shouldShowCodexApiKeyLink && codexWebsiteUrl && (
|
||||||
<div className="-mt-1 pl-1">
|
<div className="-mt-1 pl-1">
|
||||||
<a
|
<a
|
||||||
href={getCurrentCodexWebsiteUrl()}
|
href={codexWebsiteUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
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"
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ export { useApiKeyState } from "./useApiKeyState";
|
|||||||
export { useBaseUrlState } from "./useBaseUrlState";
|
export { useBaseUrlState } from "./useBaseUrlState";
|
||||||
export { useModelState } from "./useModelState";
|
export { useModelState } from "./useModelState";
|
||||||
export { useCodexConfigState } from "./useCodexConfigState";
|
export { useCodexConfigState } from "./useCodexConfigState";
|
||||||
|
export { useApiKeyLink } from "./useApiKeyLink";
|
||||||
|
export { useCustomEndpoints } from "./useCustomEndpoints";
|
||||||
|
|||||||
63
src/components/providers/forms/hooks/useApiKeyLink.ts
Normal file
63
src/components/providers/forms/hooks/useApiKeyLink.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
92
src/components/providers/forms/hooks/useCustomEndpoints.ts
Normal file
92
src/components/providers/forms/hooks/useCustomEndpoints.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user