feat: add useSpeedTestEndpoints hook to collect all endpoint candidates for speed test modal

- Create useSpeedTestEndpoints hook that collects endpoints from:
  1. Current baseUrl/codexBaseUrl
  2. Initial data URL (edit mode)
  3. Preset's endpointCandidates array
- Pass speedTestEndpoints to ClaudeFormFields and CodexFormFields
- Update EndpointSpeedTest to use collected endpoints instead of just current URL
- Fix PackyCode preset endpoints not appearing in speed test modal
This commit is contained in:
Jason
2025-10-16 22:41:36 +08:00
parent 8a724b79ec
commit 17f350f2d3
5 changed files with 175 additions and 2 deletions

View File

@@ -8,6 +8,10 @@ import { Zap } from "lucide-react";
import type { ProviderCategory } from "@/types"; import type { ProviderCategory } from "@/types";
import type { TemplateValueConfig } from "@/config/providerPresets"; import type { TemplateValueConfig } from "@/config/providerPresets";
interface EndpointCandidate {
url: string;
}
interface ClaudeFormFieldsProps { interface ClaudeFormFieldsProps {
// API Key // API Key
shouldShowApiKey: boolean; shouldShowApiKey: boolean;
@@ -48,6 +52,9 @@ interface ClaudeFormFieldsProps {
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
value: string value: string
) => void; ) => void;
// Speed Test Endpoints
speedTestEndpoints: EndpointCandidate[];
} }
export function ClaudeFormFields({ export function ClaudeFormFields({
@@ -75,6 +82,7 @@ export function ClaudeFormFields({
kimiAnthropicModel, kimiAnthropicModel,
kimiAnthropicSmallFastModel, kimiAnthropicSmallFastModel,
onKimiModelChange, onKimiModelChange,
speedTestEndpoints,
}: ClaudeFormFieldsProps) { }: ClaudeFormFieldsProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -195,7 +203,7 @@ export function ClaudeFormFields({
appType="claude" appType="claude"
value={baseUrl} value={baseUrl}
onChange={onBaseUrlChange} onChange={onBaseUrlChange}
initialEndpoints={[{ url: baseUrl }]} initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen} visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)} onClose={() => onEndpointModalToggle(false)}
onCustomEndpointsChange={onCustomEndpointsChange} onCustomEndpointsChange={onCustomEndpointsChange}

View File

@@ -6,6 +6,10 @@ import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest";
import { Zap } from "lucide-react"; import { Zap } from "lucide-react";
import type { ProviderCategory } from "@/types"; import type { ProviderCategory } from "@/types";
interface EndpointCandidate {
url: string;
}
interface CodexFormFieldsProps { interface CodexFormFieldsProps {
// API Key // API Key
codexApiKey: string; codexApiKey: string;
@@ -21,6 +25,9 @@ interface CodexFormFieldsProps {
isEndpointModalOpen: boolean; isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void; onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange: (endpoints: string[]) => void; onCustomEndpointsChange: (endpoints: string[]) => void;
// Speed Test Endpoints
speedTestEndpoints: EndpointCandidate[];
} }
export function CodexFormFields({ export function CodexFormFields({
@@ -35,6 +42,7 @@ export function CodexFormFields({
isEndpointModalOpen, isEndpointModalOpen,
onEndpointModalToggle, onEndpointModalToggle,
onCustomEndpointsChange, onCustomEndpointsChange,
speedTestEndpoints,
}: CodexFormFieldsProps) { }: CodexFormFieldsProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -120,7 +128,7 @@ export function CodexFormFields({
appType="codex" appType="codex"
value={codexBaseUrl} value={codexBaseUrl}
onChange={onBaseUrlChange} onChange={onBaseUrlChange}
initialEndpoints={[{ url: codexBaseUrl }]} initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen} visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)} onClose={() => onEndpointModalToggle(false)}
onCustomEndpointsChange={onCustomEndpointsChange} onCustomEndpointsChange={onCustomEndpointsChange}

View File

@@ -31,6 +31,7 @@ import {
useTemplateValues, useTemplateValues,
useCommonConfigSnippet, useCommonConfigSnippet,
useCodexCommonConfig, useCodexCommonConfig,
useSpeedTestEndpoints,
} from "./hooks"; } from "./hooks";
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2); const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
@@ -370,6 +371,16 @@ export function ProviderForm({
codexBaseUrl, codexBaseUrl,
}); });
// 使用端点测速候选 hook
const speedTestEndpoints = useSpeedTestEndpoints({
appType,
selectedPresetId,
presetEntries,
baseUrl,
codexBaseUrl,
initialData,
});
const handlePresetChange = (value: string) => { const handlePresetChange = (value: string) => {
setSelectedPresetId(value); setSelectedPresetId(value);
if (value === "custom") { if (value === "custom") {
@@ -470,6 +481,7 @@ export function ProviderForm({
kimiAnthropicModel={kimiAnthropicModel} kimiAnthropicModel={kimiAnthropicModel}
kimiAnthropicSmallFastModel={kimiAnthropicSmallFastModel} kimiAnthropicSmallFastModel={kimiAnthropicSmallFastModel}
onKimiModelChange={handleKimiModelChange} onKimiModelChange={handleKimiModelChange}
speedTestEndpoints={speedTestEndpoints}
/> />
)} )}
@@ -487,6 +499,7 @@ export function ProviderForm({
isEndpointModalOpen={isCodexEndpointModalOpen} isEndpointModalOpen={isCodexEndpointModalOpen}
onEndpointModalToggle={setIsCodexEndpointModalOpen} onEndpointModalToggle={setIsCodexEndpointModalOpen}
onCustomEndpointsChange={setDraftCustomEndpoints} onCustomEndpointsChange={setDraftCustomEndpoints}
speedTestEndpoints={speedTestEndpoints}
/> />
)} )}

View File

@@ -9,3 +9,4 @@ export { useKimiModelSelector } from "./useKimiModelSelector";
export { useTemplateValues } from "./useTemplateValues"; export { useTemplateValues } from "./useTemplateValues";
export { useCommonConfigSnippet } from "./useCommonConfigSnippet"; export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
export { useCodexCommonConfig } from "./useCodexCommonConfig"; export { useCodexCommonConfig } from "./useCodexCommonConfig";
export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints";

View File

@@ -0,0 +1,143 @@
import { useMemo } from "react";
import type { AppType } from "@/lib/api";
import type { ProviderPreset } from "@/config/providerPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
type PresetEntry = {
id: string;
preset: ProviderPreset | CodexProviderPreset;
};
interface UseSpeedTestEndpointsProps {
appType: AppType;
selectedPresetId: string | null;
presetEntries: PresetEntry[];
baseUrl: string;
codexBaseUrl: string;
initialData?: {
settingsConfig?: Record<string, unknown>;
};
}
export interface EndpointCandidate {
url: string;
}
/**
* 收集端点测速弹窗的初始端点列表
*
* 收集来源:
* 1. 当前选中的 Base URL
* 2. 编辑模式下的初始数据 URL
* 3. 预设中的 endpointCandidates
*/
export function useSpeedTestEndpoints({
appType,
selectedPresetId,
presetEntries,
baseUrl,
codexBaseUrl,
initialData,
}: UseSpeedTestEndpointsProps) {
const claudeEndpoints = useMemo<EndpointCandidate[]>(() => {
if (appType !== "claude") return [];
const map = new Map<string, EndpointCandidate>();
const add = (url?: string) => {
if (!url) return;
const sanitized = url.trim().replace(/\/+$/, "");
if (!sanitized || map.has(sanitized)) return;
map.set(sanitized, { url: sanitized });
};
// 1. 当前 Base URL
if (baseUrl) {
add(baseUrl);
}
// 2. 编辑模式:初始数据中的 URL
if (initialData && typeof initialData.settingsConfig === "object") {
const envUrl = (initialData.settingsConfig as any)?.env
?.ANTHROPIC_BASE_URL;
if (typeof envUrl === "string") {
add(envUrl);
}
}
// 3. 预设中的 endpointCandidates
if (selectedPresetId && selectedPresetId !== "custom") {
const entry = presetEntries.find((item) => item.id === selectedPresetId);
if (entry) {
const preset = entry.preset as ProviderPreset;
// 添加预设自己的 baseUrl
const presetEnv = (preset.settingsConfig as any)?.env
?.ANTHROPIC_BASE_URL;
if (typeof presetEnv === "string") {
add(presetEnv);
}
// 添加预设的候选端点
if (Array.isArray((preset as any).endpointCandidates)) {
for (const u of (preset as any).endpointCandidates as string[]) {
add(u);
}
}
}
}
return Array.from(map.values());
}, [appType, baseUrl, initialData, selectedPresetId, presetEntries]);
const codexEndpoints = useMemo<EndpointCandidate[]>(() => {
if (appType !== "codex") return [];
const map = new Map<string, EndpointCandidate>();
const add = (url?: string) => {
if (!url) return;
const sanitized = url.trim().replace(/\/+$/, "");
if (!sanitized || map.has(sanitized)) return;
map.set(sanitized, { url: sanitized });
};
// 1. 当前 Codex Base URL
if (codexBaseUrl) {
add(codexBaseUrl);
}
// 2. 编辑模式:初始数据中的 URL
const initialCodexConfig =
initialData && typeof initialData.settingsConfig?.config === "string"
? (initialData.settingsConfig as any).config
: "";
// 从 TOML 中提取 base_url
const match = /base_url\s*=\s*["']([^"']+)["']/i.exec(initialCodexConfig);
if (match?.[1]) {
add(match[1]);
}
// 3. 预设中的 endpointCandidates
if (selectedPresetId && selectedPresetId !== "custom") {
const entry = presetEntries.find((item) => item.id === selectedPresetId);
if (entry) {
const preset = entry.preset as CodexProviderPreset;
// 添加预设自己的 baseUrl
const presetConfig = preset.config || "";
const presetMatch = /base_url\s*=\s*["']([^"']+)["']/i.exec(
presetConfig
);
if (presetMatch?.[1]) {
add(presetMatch[1]);
}
// 添加预设的候选端点
if (Array.isArray((preset as any).endpointCandidates)) {
for (const u of (preset as any).endpointCandidates as string[]) {
add(u);
}
}
}
}
return Array.from(map.values());
}, [appType, codexBaseUrl, initialData, selectedPresetId, presetEntries]);
return appType === "codex" ? codexEndpoints : claudeEndpoints;
}