import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Zap, Loader2, Plus, Trash2, AlertCircle, Check } from "lucide-react"; import type { AppType } from "../../lib/tauri-api"; export interface EndpointCandidate { id?: string; url: string; label?: string; isCustom?: boolean; } interface EndpointSpeedTestProps { appType: AppType; value: string; onChange: (url: string) => void; initialEndpoints: EndpointCandidate[]; visible?: boolean; } interface EndpointEntry extends EndpointCandidate { id: string; latency: number | null; status?: number; error?: string | null; } const randomId = () => `ep_${Math.random().toString(36).slice(2, 9)}`; const normalizeEndpointUrl = (url: string): string => url.trim().replace(/\/+$/, ""); const buildInitialEntries = ( candidates: EndpointCandidate[], selected: string, ): EndpointEntry[] => { const map = new Map(); const addCandidate = (candidate: EndpointCandidate) => { const sanitized = candidate.url ? normalizeEndpointUrl(candidate.url) : ""; if (!sanitized) return; if (map.has(sanitized)) { const existing = map.get(sanitized)!; if (candidate.label && candidate.label !== existing.label) { map.set(sanitized, { ...existing, label: candidate.label }); } return; } const index = map.size; const label = candidate.label ?? (candidate.isCustom ? `自定义 ${index + 1}` : index === 0 ? "默认地址" : `候选 ${index + 1}`); map.set(sanitized, { id: candidate.id ?? randomId(), url: sanitized, label, isCustom: candidate.isCustom ?? false, latency: null, status: undefined, error: null, }); }; candidates.forEach(addCandidate); const selectedUrl = normalizeEndpointUrl(selected); if (selectedUrl && !map.has(selectedUrl)) { addCandidate({ url: selectedUrl, label: "当前地址", isCustom: true }); } return Array.from(map.values()); }; const EndpointSpeedTest: React.FC = ({ appType, value, onChange, initialEndpoints, visible = true, }) => { const [entries, setEntries] = useState(() => buildInitialEntries(initialEndpoints, value), ); const [customUrl, setCustomUrl] = useState(""); const [addError, setAddError] = useState(null); const [autoSelect, setAutoSelect] = useState(true); const [isTesting, setIsTesting] = useState(false); const [lastError, setLastError] = useState(null); const normalizedSelected = normalizeEndpointUrl(value); const hasEndpoints = entries.length > 0; useEffect(() => { setEntries((prev) => { const map = new Map(); prev.forEach((entry) => { map.set(entry.url, entry); }); let changed = false; const mergeCandidate = (candidate: EndpointCandidate) => { const sanitized = candidate.url ? normalizeEndpointUrl(candidate.url) : ""; if (!sanitized) return; const existing = map.get(sanitized); if (existing) { if (candidate.label && candidate.label !== existing.label) { map.set(sanitized, { ...existing, label: candidate.label }); changed = true; } return; } const index = map.size; const label = candidate.label ?? (candidate.isCustom ? `自定义 ${index + 1}` : index === 0 ? "默认地址" : `候选 ${index + 1}`); map.set(sanitized, { id: candidate.id ?? randomId(), url: sanitized, label, isCustom: candidate.isCustom ?? false, latency: null, status: undefined, error: null, }); changed = true; }; initialEndpoints.forEach(mergeCandidate); if (normalizedSelected) { const existing = map.get(normalizedSelected); if (existing) { if (existing.label !== "当前地址") { map.set(normalizedSelected, { ...existing, label: existing.isCustom ? existing.label : "当前地址", }); changed = true; } } else { mergeCandidate({ url: normalizedSelected, label: "当前地址", isCustom: true }); } } if (!changed) { return prev; } return Array.from(map.values()); }); }, [initialEndpoints, normalizedSelected]); const sortedEntries = useMemo(() => { return entries.slice().sort((a, b) => { const aLatency = a.latency ?? Number.POSITIVE_INFINITY; const bLatency = b.latency ?? Number.POSITIVE_INFINITY; if (aLatency === bLatency) { return a.url.localeCompare(b.url); } return aLatency - bLatency; }); }, [entries]); const handleAddEndpoint = useCallback(() => { const candidate = customUrl.trim(); setAddError(null); if (!candidate) { setAddError("请输入有效的 URL"); return; } let parsed: URL; try { parsed = new URL(candidate); } catch { setAddError("URL 格式不正确,请确认包含 http(s) 前缀"); return; } if (!parsed.protocol.startsWith("http")) { setAddError("仅支持 HTTP/HTTPS 地址"); return; } const sanitized = normalizeEndpointUrl(parsed.toString()); setEntries((prev) => { if (prev.some((entry) => entry.url === sanitized)) { setAddError("该地址已存在"); return prev; } const customCount = prev.filter((entry) => entry.isCustom).length; return [ ...prev, { id: randomId(), url: sanitized, label: `自定义 ${customCount + 1}`, isCustom: true, latency: null, status: undefined, error: null, }, ]; }); if (!normalizedSelected) { onChange(sanitized); } setCustomUrl(""); }, [customUrl, normalizedSelected, onChange]); const handleRemoveEndpoint = useCallback( (entry: EndpointEntry) => { setEntries((prev) => { const next = prev.filter((item) => item.id !== entry.id); if (entry.url === normalizedSelected) { const fallback = next[0]; onChange(fallback ? fallback.url : ""); } return next; }); }, [normalizedSelected, onChange], ); const runSpeedTest = useCallback(async () => { const urls = entries.map((entry) => entry.url); if (urls.length === 0) { setLastError("请先添加至少一个地址再进行测速"); return; } if (typeof window === "undefined" || !window.api?.testApiEndpoints) { setLastError("测速功能仅在桌面应用中可用"); return; } setIsTesting(true); setLastError(null); try { const results = await window.api.testApiEndpoints(urls, { timeoutSecs: appType === "codex" ? 12 : 8, }); const resultMap = new Map( results.map((item) => [normalizeEndpointUrl(item.url), item]), ); setEntries((prev) => prev.map((entry) => { const match = resultMap.get(entry.url); if (!match) { return { ...entry, latency: null, status: undefined, error: "未返回测速结果", }; } return { ...entry, latency: typeof match.latency === "number" ? Math.round(match.latency) : null, status: match.status, error: match.error ?? null, }; }), ); if (autoSelect) { const successful = results .filter((item) => typeof item.latency === "number" && item.latency !== null) .sort((a, b) => (a.latency! || 0) - (b.latency! || 0)); const best = successful[0]; if (best && best.url && best.url !== normalizedSelected) { onChange(best.url); } } } catch (error) { const message = error instanceof Error ? error.message : `测速失败: ${String(error)}`; setLastError(message); } finally { setIsTesting(false); } }, [entries, autoSelect, appType, normalizedSelected, onChange]); const handleSelect = useCallback( (url: string) => { if (!url || url === normalizedSelected) return; onChange(url); }, [normalizedSelected, onChange], ); if (!visible) { return null; } return (

节点测速

添加多个端点后可一键测速,自动选取延迟最低的地址

{hasEndpoints ? (
{sortedEntries.map((entry) => { const isSelected = normalizedSelected === entry.url; const latency = entry.latency; const statusBadge = latency !== null ? latency <= 100 ? "text-green-600 dark:text-green-400" : latency <= 300 ? "text-amber-600 dark:text-amber-400" : "text-red-600 dark:text-red-400" : "text-gray-500 dark:text-gray-400"; return (
{latency !== null ? ( {latency} ms ) : isTesting ? ( 等待结果 ) : entry.error ? ( 失败 ) : ( 未测速 )}
{entry.isCustom && ( )}
); })}
) : (
暂无可测速的地址,请先添加至少一个请求地址
)}
setCustomUrl(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); handleAddEndpoint(); } }} className="flex-1 rounded-md border border-gray-200 px-3 py-2 text-sm text-gray-800 shadow-sm transition focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400/20 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100" />
{addError && (

{addError}

)}
{lastError && (

{lastError}

)}
); }; export default EndpointSpeedTest;