import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Zap, Loader2, Plus, X, AlertCircle } from "lucide-react"; import { isLinux } from "../../lib/platform"; import type { AppType } from "../../lib/tauri-api"; export interface EndpointCandidate { id?: string; url: string; isCustom?: boolean; } interface EndpointSpeedTestProps { appType: AppType; value: string; onChange: (url: string) => void; initialEndpoints: EndpointCandidate[]; visible?: boolean; onClose: () => void; } 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)) return; map.set(sanitized, { id: candidate.id ?? randomId(), url: sanitized, 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, isCustom: true }); } return Array.from(map.values()); }; const EndpointSpeedTest: React.FC = ({ appType, value, onChange, initialEndpoints, visible = true, onClose, }) => { 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(() => { const loadCustomEndpoints = async () => { try { const customEndpoints = await window.api.getCustomEndpoints(appType); const candidates: EndpointCandidate[] = customEndpoints.map((ep) => ({ url: ep.url, isCustom: true, })); setEntries((prev) => { const map = new Map(); // 先添加现有端点 prev.forEach((entry) => { map.set(entry.url, entry); }); // 合并自定义端点 candidates.forEach((candidate) => { const sanitized = normalizeEndpointUrl(candidate.url); if (sanitized && !map.has(sanitized)) { map.set(sanitized, { id: randomId(), url: sanitized, isCustom: true, latency: null, status: undefined, error: null, }); } }); return Array.from(map.values()); }); } catch (error) { console.error("加载自定义端点失败:", error); } }; if (visible) { loadCustomEndpoints(); } }, [appType, visible]); 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) return; map.set(sanitized, { id: candidate.id ?? randomId(), url: sanitized, isCustom: candidate.isCustom ?? false, latency: null, status: undefined, error: null, }); changed = true; }; initialEndpoints.forEach(mergeCandidate); if (normalizedSelected && !map.has(normalizedSelected)) { mergeCandidate({ url: normalizedSelected, 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( async () => { const candidate = customUrl.trim(); setAddError(null); if (!candidate) { setAddError("请输入有效的 URL"); return; } let parsed: URL; try { parsed = new URL(candidate); } catch { setAddError("URL 格式不正确"); 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; } return prev; }); if (addError) return; // 保存到后端 try { await window.api.addCustomEndpoint(appType, sanitized); // 更新本地状态 setEntries((prev) => [ ...prev, { id: randomId(), url: sanitized, isCustom: true, latency: null, status: undefined, error: null, }, ]); if (!normalizedSelected) { onChange(sanitized); } setCustomUrl(""); } catch (error) { setAddError("保存失败,请重试"); console.error("添加自定义端点失败:", error); } }, [customUrl, normalizedSelected, onChange, appType, addError], ); const handleRemoveEndpoint = useCallback( async (entry: EndpointEntry) => { // 如果是自定义端点,从后端删除 if (entry.isCustom) { try { await window.api.removeCustomEndpoint(appType, entry.url); } catch (error) { console.error("删除自定义端点失败:", error); return; } } // 更新本地状态 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, appType], ); 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( async (url: string) => { if (!url || url === normalizedSelected) return; // 更新最后使用时间(对自定义端点) const entry = entries.find((e) => e.url === url); if (entry?.isCustom) { await window.api.updateEndpointLastUsed(appType, url); } onChange(url); }, [normalizedSelected, onChange, appType, entries], ); // 支持按下 ESC 关闭弹窗 useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); onClose(); } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [onClose]); if (!visible) { return null; } return (
{ if (e.target === e.currentTarget) onClose(); }} > {/* Backdrop */}
{/* Modal */}
{/* Header */}

请求地址管理

{/* Content */}
{/* 测速控制栏 */}
{entries.length} 个端点
{/* 添加输入 */}
setCustomUrl(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); handleAddEndpoint(); } }} className="flex-1 rounded-md border border-gray-200 bg-white px-3 py-1.5 text-sm text-gray-900 placeholder-gray-400 transition focus:border-gray-400 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-gray-600" />
{addError && (
{addError}
)}
{/* 端点列表 */} {hasEndpoints ? (
{sortedEntries.map((entry) => { const isSelected = normalizedSelected === entry.url; const latency = entry.latency; return (
handleSelect(entry.url)} className={`group flex cursor-pointer items-center justify-between px-3 py-2.5 rounded-lg border transition ${ isSelected ? "border-blue-500 bg-blue-50 dark:border-blue-500 dark:bg-blue-900/20" : "border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:hover:border-gray-600 dark:hover:bg-gray-850" }`} >
{/* 选择指示器 */}
{/* 内容 */}
{entry.url}
{/* 右侧信息 */}
{latency !== null ? (
{latency}ms
) : isTesting ? ( ) : entry.error ? (
失败
) : (
)}
); })}
) : (
暂无端点
)} {/* 错误提示 */} {lastError && (
{lastError}
)}
{/* Footer */}
); }; export default EndpointSpeedTest;