feat: Implement Speed Test Function
* feat: add unified endpoint speed test for API providers
Add a comprehensive endpoint latency testing system that allows users to:
- Test multiple API endpoints concurrently
- Auto-select the fastest endpoint based on latency
- Add/remove custom endpoints dynamically
- View latency results with color-coded indicators
Backend (Rust):
- Implement parallel HTTP HEAD requests with configurable timeout
- Handle various error scenarios (timeout, connection failure, invalid URL)
- Return structured latency data with status codes
Frontend (React):
- Create interactive speed test UI component with auto-sort by latency
- Support endpoint management (add/remove custom endpoints)
- Extract and update Codex base_url from TOML configuration
- Integrate with provider presets for default endpoint candidates
This feature improves user experience when selecting optimal API endpoints,
especially useful for users with multiple provider options or proxy setups.
* refactor: convert endpoint speed test to modal dialog
- Transform EndpointSpeedTest component into a modal dialog
- Add "Advanced" button next to base URL input to open modal
- Support ESC key and backdrop click to close modal
- Apply Linear design principles: minimal styling, clean layout
- Remove unused showBaseUrlInput variable
- Implement same modal pattern for both Claude and Codex
* fix: prevent modal cascade closing when ESC is pressed
- Add state checks to prevent parent modal from closing when child modals (endpoint speed test or template wizard) are open
- Update ESC key handler dependencies to track all modal states
- Ensures only the topmost modal responds to ESC key
* refactor: unify speed test panel UI with project design system
UI improvements:
- Update modal border radius from rounded-lg to rounded-xl
- Unify header padding from px-6 py-4 to p-6
- Change speed test button color to blue theme (bg-blue-500) for consistency
- Update footer background from bg-gray-50 to bg-gray-100
- Style "Done" button as primary action button with blue theme
- Adjust footer button spacing and hover states
Simplify endpoint display:
- Remove endpoint labels (e.g., "Current Address", "Custom 1")
- Display only URL for cleaner interface
- Clean up all label-related logic:
* Remove label field from EndpointCandidate interface
* Remove label generation in buildInitialEntries function
* Remove label handling in useEffect merge logic
* Remove label generation in handleAddEndpoint
* Remove label parameters from claudeSpeedTestEndpoints
* Remove label parameters from codexSpeedTestEndpoints
* refactor: improve endpoint list UI consistency
- Show delete button for all endpoints on hover for uniform UI
- Change selected state to use blue theme matching main interface:
* Blue border (border-blue-500) for selected items
* Light blue background (bg-blue-50/dark:bg-blue-900/20)
* Blue indicator dot (bg-blue-500/dark:bg-blue-400)
- Switch from compact list (space-y-px) to card-based layout (space-y-2)
- Add rounded corners to each endpoint item for better visual separation
* feat: persist custom endpoints to settings.json
- Extend AppSettings to store custom endpoints for Claude and Codex
- Add Tauri commands: get/add/remove/update custom endpoints
- Update frontend API with endpoint persistence methods
- Modify EndpointSpeedTest to load/save custom endpoints via API
- Track endpoint last used time for future sorting/cleanup
- Store endpoints per app type in settings.json instead of localStorage
* - feat(types): add Provider.meta and ProviderMeta (snake_case) with custom_endpoints map
- feat(provider-form): persist custom endpoints on provider create by merging EndpointSpeedTest’s custom URLs into meta.custom_endpoints on submit
- feat(endpoint-speed-test): add onCustomEndpointsChange callback emitting normalized custom URLs; wire it for both Claude/Codex modals
- fix(api): send alias param names (app/appType/app_type and provider_id/providerId) in Tauri invokes to avoid “missing providerId” with older backends
- storage: custom endpoints are stored in ~/.cc-switch/config.json under providers[<id>].meta.custom_endpoints (not in settings.json)
- behavior: edit flow remains immediate writes; create flow now writes once via addProvider, removing the providerId dependency during creation
* feat: add endpoint candidates support and code formatting improvements
- Add endpointCandidates field to ProviderPreset and CodexProviderPreset interfaces
- Integrate preset endpoint candidates into speed test endpoint selection
- Add multiple endpoint options for PackyCode providers (Claude & Codex)
- Apply consistent code formatting (trailing commas, line breaks)
- Improve template value type safety and readability
* refactor: improve endpoint management button UX
Replace ambiguous "Advanced" text with intuitive "Manage & Test" label accompanied by Zap icon, making the endpoint management panel entry point more discoverable and self-explanatory for both Claude and Codex configurations.
* - merge: merge origin/main, resolve conflicts and preserve both feature sets
- feat(tauri): register import/export and file dialogs; keep endpoint speed test and custom endpoints
- feat(api): add updateTrayMenu and onProviderSwitched; wire import/export APIs
- feat(types): extend global API declarations (import/export)
- chore(presets): GLM preset supports both new and legacy model keys
- chore(rust): add chrono dependency; refresh lockfile
---------
Co-authored-by: Jason <farion1231@gmail.com>
2025-10-07 19:14:32 +08:00
|
|
|
|
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;
|
|
|
|
|
|
providerId?: string;
|
|
|
|
|
|
value: string;
|
|
|
|
|
|
onChange: (url: string) => void;
|
|
|
|
|
|
initialEndpoints: EndpointCandidate[];
|
|
|
|
|
|
visible?: boolean;
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
// 当自定义端点列表变化时回传(仅包含 isCustom 的条目)
|
|
|
|
|
|
onCustomEndpointsChange?: (urls: string[]) => 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<string, EndpointEntry>();
|
|
|
|
|
|
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<EndpointSpeedTestProps> = ({
|
|
|
|
|
|
appType,
|
|
|
|
|
|
providerId,
|
|
|
|
|
|
value,
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
initialEndpoints,
|
|
|
|
|
|
visible = true,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
onCustomEndpointsChange,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const [entries, setEntries] = useState<EndpointEntry[]>(() =>
|
|
|
|
|
|
buildInitialEntries(initialEndpoints, value),
|
|
|
|
|
|
);
|
|
|
|
|
|
const [customUrl, setCustomUrl] = useState("");
|
|
|
|
|
|
const [addError, setAddError] = useState<string | null>(null);
|
|
|
|
|
|
const [autoSelect, setAutoSelect] = useState(true);
|
|
|
|
|
|
const [isTesting, setIsTesting] = useState(false);
|
|
|
|
|
|
const [lastError, setLastError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedSelected = normalizeEndpointUrl(value);
|
|
|
|
|
|
|
|
|
|
|
|
const hasEndpoints = entries.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 加载保存的自定义端点(按正在编辑的供应商)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadCustomEndpoints = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!providerId) return;
|
|
|
|
|
|
const customEndpoints = await window.api.getCustomEndpoints(
|
|
|
|
|
|
appType,
|
|
|
|
|
|
providerId,
|
|
|
|
|
|
);
|
|
|
|
|
|
const candidates: EndpointCandidate[] = customEndpoints.map((ep) => ({
|
|
|
|
|
|
url: ep.url,
|
|
|
|
|
|
isCustom: true,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
setEntries((prev) => {
|
|
|
|
|
|
const map = new Map<string, EndpointEntry>();
|
|
|
|
|
|
|
|
|
|
|
|
// 先添加现有端点
|
|
|
|
|
|
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, providerId]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setEntries((prev) => {
|
|
|
|
|
|
const map = new Map<string, EndpointEntry>();
|
|
|
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
|
|
// 将自定义端点变化透传给父组件(仅限 isCustom)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!onCustomEndpointsChange) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const customUrls = Array.from(
|
|
|
|
|
|
new Set(
|
|
|
|
|
|
entries
|
|
|
|
|
|
.filter((e) => e.isCustom)
|
|
|
|
|
|
.map((e) => (e.url ? normalizeEndpointUrl(e.url) : ""))
|
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
onCustomEndpointsChange(customUrls);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
// 仅在 entries 变化时同步
|
|
|
|
|
|
}, [entries, onCustomEndpointsChange]);
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
let errorMsg: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (!candidate) {
|
|
|
|
|
|
errorMsg = "请输入有效的 URL";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let parsed: URL | null = null;
|
|
|
|
|
|
if (!errorMsg) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
parsed = new URL(candidate);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
errorMsg = "URL 格式不正确";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!errorMsg && parsed && !parsed.protocol.startsWith("http")) {
|
|
|
|
|
|
errorMsg = "仅支持 HTTP/HTTPS";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let sanitized = "";
|
|
|
|
|
|
if (!errorMsg && parsed) {
|
|
|
|
|
|
sanitized = normalizeEndpointUrl(parsed.toString());
|
|
|
|
|
|
// 使用当前 entries 做去重校验,避免依赖可能过期的 addError
|
|
|
|
|
|
const isDuplicate = entries.some((entry) => entry.url === sanitized);
|
|
|
|
|
|
if (isDuplicate) {
|
|
|
|
|
|
errorMsg = "该地址已存在";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (errorMsg) {
|
|
|
|
|
|
setAddError(errorMsg);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setAddError(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 保存到后端
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (providerId) {
|
|
|
|
|
|
await window.api.addCustomEndpoint(appType, providerId, sanitized);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地状态
|
|
|
|
|
|
setEntries((prev) => {
|
|
|
|
|
|
if (prev.some((e) => e.url === sanitized)) return prev;
|
|
|
|
|
|
return [
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
{
|
|
|
|
|
|
id: randomId(),
|
|
|
|
|
|
url: sanitized,
|
|
|
|
|
|
isCustom: true,
|
|
|
|
|
|
latency: null,
|
|
|
|
|
|
status: undefined,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!normalizedSelected) {
|
|
|
|
|
|
onChange(sanitized);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setCustomUrl("");
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
const message =
|
|
|
|
|
|
error instanceof Error ? error.message : String(error);
|
|
|
|
|
|
setAddError(message || "保存失败,请重试");
|
|
|
|
|
|
console.error("添加自定义端点失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[customUrl, entries, normalizedSelected, onChange, appType, providerId],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleRemoveEndpoint = useCallback(
|
|
|
|
|
|
async (entry: EndpointEntry) => {
|
|
|
|
|
|
// 如果是自定义端点,尝试从后端删除(无 providerId 则仅本地删除)
|
|
|
|
|
|
if (entry.isCustom && providerId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await window.api.removeCustomEndpoint(appType, providerId, 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, providerId],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2025-10-07 20:51:12 +08:00
|
|
|
|
// 清空所有延迟数据,显示 loading 状态
|
|
|
|
|
|
setEntries((prev) =>
|
|
|
|
|
|
prev.map((entry) => ({
|
|
|
|
|
|
...entry,
|
|
|
|
|
|
latency: null,
|
|
|
|
|
|
status: undefined,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
feat: Implement Speed Test Function
* feat: add unified endpoint speed test for API providers
Add a comprehensive endpoint latency testing system that allows users to:
- Test multiple API endpoints concurrently
- Auto-select the fastest endpoint based on latency
- Add/remove custom endpoints dynamically
- View latency results with color-coded indicators
Backend (Rust):
- Implement parallel HTTP HEAD requests with configurable timeout
- Handle various error scenarios (timeout, connection failure, invalid URL)
- Return structured latency data with status codes
Frontend (React):
- Create interactive speed test UI component with auto-sort by latency
- Support endpoint management (add/remove custom endpoints)
- Extract and update Codex base_url from TOML configuration
- Integrate with provider presets for default endpoint candidates
This feature improves user experience when selecting optimal API endpoints,
especially useful for users with multiple provider options or proxy setups.
* refactor: convert endpoint speed test to modal dialog
- Transform EndpointSpeedTest component into a modal dialog
- Add "Advanced" button next to base URL input to open modal
- Support ESC key and backdrop click to close modal
- Apply Linear design principles: minimal styling, clean layout
- Remove unused showBaseUrlInput variable
- Implement same modal pattern for both Claude and Codex
* fix: prevent modal cascade closing when ESC is pressed
- Add state checks to prevent parent modal from closing when child modals (endpoint speed test or template wizard) are open
- Update ESC key handler dependencies to track all modal states
- Ensures only the topmost modal responds to ESC key
* refactor: unify speed test panel UI with project design system
UI improvements:
- Update modal border radius from rounded-lg to rounded-xl
- Unify header padding from px-6 py-4 to p-6
- Change speed test button color to blue theme (bg-blue-500) for consistency
- Update footer background from bg-gray-50 to bg-gray-100
- Style "Done" button as primary action button with blue theme
- Adjust footer button spacing and hover states
Simplify endpoint display:
- Remove endpoint labels (e.g., "Current Address", "Custom 1")
- Display only URL for cleaner interface
- Clean up all label-related logic:
* Remove label field from EndpointCandidate interface
* Remove label generation in buildInitialEntries function
* Remove label handling in useEffect merge logic
* Remove label generation in handleAddEndpoint
* Remove label parameters from claudeSpeedTestEndpoints
* Remove label parameters from codexSpeedTestEndpoints
* refactor: improve endpoint list UI consistency
- Show delete button for all endpoints on hover for uniform UI
- Change selected state to use blue theme matching main interface:
* Blue border (border-blue-500) for selected items
* Light blue background (bg-blue-50/dark:bg-blue-900/20)
* Blue indicator dot (bg-blue-500/dark:bg-blue-400)
- Switch from compact list (space-y-px) to card-based layout (space-y-2)
- Add rounded corners to each endpoint item for better visual separation
* feat: persist custom endpoints to settings.json
- Extend AppSettings to store custom endpoints for Claude and Codex
- Add Tauri commands: get/add/remove/update custom endpoints
- Update frontend API with endpoint persistence methods
- Modify EndpointSpeedTest to load/save custom endpoints via API
- Track endpoint last used time for future sorting/cleanup
- Store endpoints per app type in settings.json instead of localStorage
* - feat(types): add Provider.meta and ProviderMeta (snake_case) with custom_endpoints map
- feat(provider-form): persist custom endpoints on provider create by merging EndpointSpeedTest’s custom URLs into meta.custom_endpoints on submit
- feat(endpoint-speed-test): add onCustomEndpointsChange callback emitting normalized custom URLs; wire it for both Claude/Codex modals
- fix(api): send alias param names (app/appType/app_type and provider_id/providerId) in Tauri invokes to avoid “missing providerId” with older backends
- storage: custom endpoints are stored in ~/.cc-switch/config.json under providers[<id>].meta.custom_endpoints (not in settings.json)
- behavior: edit flow remains immediate writes; create flow now writes once via addProvider, removing the providerId dependency during creation
* feat: add endpoint candidates support and code formatting improvements
- Add endpointCandidates field to ProviderPreset and CodexProviderPreset interfaces
- Integrate preset endpoint candidates into speed test endpoint selection
- Add multiple endpoint options for PackyCode providers (Claude & Codex)
- Apply consistent code formatting (trailing commas, line breaks)
- Improve template value type safety and readability
* refactor: improve endpoint management button UX
Replace ambiguous "Advanced" text with intuitive "Manage & Test" label accompanied by Zap icon, making the endpoint management panel entry point more discoverable and self-explanatory for both Claude and Codex configurations.
* - merge: merge origin/main, resolve conflicts and preserve both feature sets
- feat(tauri): register import/export and file dialogs; keep endpoint speed test and custom endpoints
- feat(api): add updateTrayMenu and onProviderSwitched; wire import/export APIs
- feat(types): extend global API declarations (import/export)
- chore(presets): GLM preset supports both new and legacy model keys
- chore(rust): add chrono dependency; refresh lockfile
---------
Co-authored-by: Jason <farion1231@gmail.com>
2025-10-07 19:14:32 +08:00
|
|
|
|
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 && providerId) {
|
|
|
|
|
|
await window.api.updateEndpointLastUsed(appType, providerId, url);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onChange(url);
|
|
|
|
|
|
},
|
|
|
|
|
|
[normalizedSelected, onChange, appType, entries, providerId],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 支持按下 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 (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
|
|
|
|
onMouseDown={(e) => {
|
|
|
|
|
|
if (e.target === e.currentTarget) onClose();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Backdrop */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
|
|
|
|
|
isLinux() ? "" : " backdrop-blur-sm"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Modal */}
|
|
|
|
|
|
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg w-full max-w-2xl mx-4 max-h-[80vh] overflow-hidden flex flex-col">
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
|
|
|
|
|
<h3 className="text-base font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
请求地址管理
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
|
|
|
|
|
aria-label="关闭"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X size={16} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
|
<div className="flex-1 overflow-auto px-6 py-4 space-y-4">
|
|
|
|
|
|
{/* 测速控制栏 */}
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
{entries.length} 个端点
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<label className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={autoSelect}
|
|
|
|
|
|
onChange={(event) => setAutoSelect(event.target.checked)}
|
|
|
|
|
|
className="h-3.5 w-3.5 rounded border-gray-300 dark:border-gray-600"
|
|
|
|
|
|
/>
|
|
|
|
|
|
自动选择
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={runSpeedTest}
|
|
|
|
|
|
disabled={isTesting || !hasEndpoints}
|
2025-10-07 20:51:12 +08:00
|
|
|
|
className="flex h-7 w-20 items-center justify-center gap-1.5 rounded-md bg-blue-500 px-2.5 text-xs font-medium text-white transition hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-blue-600 dark:hover:bg-blue-700"
|
feat: Implement Speed Test Function
* feat: add unified endpoint speed test for API providers
Add a comprehensive endpoint latency testing system that allows users to:
- Test multiple API endpoints concurrently
- Auto-select the fastest endpoint based on latency
- Add/remove custom endpoints dynamically
- View latency results with color-coded indicators
Backend (Rust):
- Implement parallel HTTP HEAD requests with configurable timeout
- Handle various error scenarios (timeout, connection failure, invalid URL)
- Return structured latency data with status codes
Frontend (React):
- Create interactive speed test UI component with auto-sort by latency
- Support endpoint management (add/remove custom endpoints)
- Extract and update Codex base_url from TOML configuration
- Integrate with provider presets for default endpoint candidates
This feature improves user experience when selecting optimal API endpoints,
especially useful for users with multiple provider options or proxy setups.
* refactor: convert endpoint speed test to modal dialog
- Transform EndpointSpeedTest component into a modal dialog
- Add "Advanced" button next to base URL input to open modal
- Support ESC key and backdrop click to close modal
- Apply Linear design principles: minimal styling, clean layout
- Remove unused showBaseUrlInput variable
- Implement same modal pattern for both Claude and Codex
* fix: prevent modal cascade closing when ESC is pressed
- Add state checks to prevent parent modal from closing when child modals (endpoint speed test or template wizard) are open
- Update ESC key handler dependencies to track all modal states
- Ensures only the topmost modal responds to ESC key
* refactor: unify speed test panel UI with project design system
UI improvements:
- Update modal border radius from rounded-lg to rounded-xl
- Unify header padding from px-6 py-4 to p-6
- Change speed test button color to blue theme (bg-blue-500) for consistency
- Update footer background from bg-gray-50 to bg-gray-100
- Style "Done" button as primary action button with blue theme
- Adjust footer button spacing and hover states
Simplify endpoint display:
- Remove endpoint labels (e.g., "Current Address", "Custom 1")
- Display only URL for cleaner interface
- Clean up all label-related logic:
* Remove label field from EndpointCandidate interface
* Remove label generation in buildInitialEntries function
* Remove label handling in useEffect merge logic
* Remove label generation in handleAddEndpoint
* Remove label parameters from claudeSpeedTestEndpoints
* Remove label parameters from codexSpeedTestEndpoints
* refactor: improve endpoint list UI consistency
- Show delete button for all endpoints on hover for uniform UI
- Change selected state to use blue theme matching main interface:
* Blue border (border-blue-500) for selected items
* Light blue background (bg-blue-50/dark:bg-blue-900/20)
* Blue indicator dot (bg-blue-500/dark:bg-blue-400)
- Switch from compact list (space-y-px) to card-based layout (space-y-2)
- Add rounded corners to each endpoint item for better visual separation
* feat: persist custom endpoints to settings.json
- Extend AppSettings to store custom endpoints for Claude and Codex
- Add Tauri commands: get/add/remove/update custom endpoints
- Update frontend API with endpoint persistence methods
- Modify EndpointSpeedTest to load/save custom endpoints via API
- Track endpoint last used time for future sorting/cleanup
- Store endpoints per app type in settings.json instead of localStorage
* - feat(types): add Provider.meta and ProviderMeta (snake_case) with custom_endpoints map
- feat(provider-form): persist custom endpoints on provider create by merging EndpointSpeedTest’s custom URLs into meta.custom_endpoints on submit
- feat(endpoint-speed-test): add onCustomEndpointsChange callback emitting normalized custom URLs; wire it for both Claude/Codex modals
- fix(api): send alias param names (app/appType/app_type and provider_id/providerId) in Tauri invokes to avoid “missing providerId” with older backends
- storage: custom endpoints are stored in ~/.cc-switch/config.json under providers[<id>].meta.custom_endpoints (not in settings.json)
- behavior: edit flow remains immediate writes; create flow now writes once via addProvider, removing the providerId dependency during creation
* feat: add endpoint candidates support and code formatting improvements
- Add endpointCandidates field to ProviderPreset and CodexProviderPreset interfaces
- Integrate preset endpoint candidates into speed test endpoint selection
- Add multiple endpoint options for PackyCode providers (Claude & Codex)
- Apply consistent code formatting (trailing commas, line breaks)
- Improve template value type safety and readability
* refactor: improve endpoint management button UX
Replace ambiguous "Advanced" text with intuitive "Manage & Test" label accompanied by Zap icon, making the endpoint management panel entry point more discoverable and self-explanatory for both Claude and Codex configurations.
* - merge: merge origin/main, resolve conflicts and preserve both feature sets
- feat(tauri): register import/export and file dialogs; keep endpoint speed test and custom endpoints
- feat(api): add updateTrayMenu and onProviderSwitched; wire import/export APIs
- feat(types): extend global API declarations (import/export)
- chore(presets): GLM preset supports both new and legacy model keys
- chore(rust): add chrono dependency; refresh lockfile
---------
Co-authored-by: Jason <farion1231@gmail.com>
2025-10-07 19:14:32 +08:00
|
|
|
|
>
|
|
|
|
|
|
{isTesting ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
|
|
|
|
测速中
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Zap className="h-3.5 w-3.5" />
|
|
|
|
|
|
测速
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 添加输入 */}
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
value={customUrl}
|
|
|
|
|
|
placeholder="https://api.example.com"
|
|
|
|
|
|
onChange={(event) => 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"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleAddEndpoint}
|
|
|
|
|
|
className="flex h-8 w-8 items-center justify-center rounded-md border border-gray-200 transition hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600 dark:hover:bg-gray-800"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{addError && (
|
|
|
|
|
|
<div className="flex items-center gap-1.5 text-xs text-red-600 dark:text-red-400">
|
|
|
|
|
|
<AlertCircle className="h-3 w-3" />
|
|
|
|
|
|
{addError}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 端点列表 */}
|
|
|
|
|
|
{hasEndpoints ? (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{sortedEntries.map((entry) => {
|
|
|
|
|
|
const isSelected = normalizedSelected === entry.url;
|
|
|
|
|
|
const latency = entry.latency;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={entry.id}
|
|
|
|
|
|
onClick={() => 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"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
|
|
|
|
{/* 选择指示器 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`h-1.5 w-1.5 flex-shrink-0 rounded-full transition ${
|
|
|
|
|
|
isSelected
|
|
|
|
|
|
? "bg-blue-500 dark:bg-blue-400"
|
|
|
|
|
|
: "bg-gray-300 dark:bg-gray-700"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 内容 */}
|
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
|
<div className="truncate text-sm text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{entry.url}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 右侧信息 */}
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
{latency !== null ? (
|
|
|
|
|
|
<div className="text-right">
|
2025-10-07 20:51:12 +08:00
|
|
|
|
<div className={`font-mono text-sm font-medium ${
|
|
|
|
|
|
latency < 300
|
|
|
|
|
|
? "text-green-600 dark:text-green-400"
|
|
|
|
|
|
: latency < 500
|
|
|
|
|
|
? "text-yellow-600 dark:text-yellow-400"
|
|
|
|
|
|
: latency < 800
|
|
|
|
|
|
? "text-orange-600 dark:text-orange-400"
|
|
|
|
|
|
: "text-red-600 dark:text-red-400"
|
|
|
|
|
|
}`}>
|
feat: Implement Speed Test Function
* feat: add unified endpoint speed test for API providers
Add a comprehensive endpoint latency testing system that allows users to:
- Test multiple API endpoints concurrently
- Auto-select the fastest endpoint based on latency
- Add/remove custom endpoints dynamically
- View latency results with color-coded indicators
Backend (Rust):
- Implement parallel HTTP HEAD requests with configurable timeout
- Handle various error scenarios (timeout, connection failure, invalid URL)
- Return structured latency data with status codes
Frontend (React):
- Create interactive speed test UI component with auto-sort by latency
- Support endpoint management (add/remove custom endpoints)
- Extract and update Codex base_url from TOML configuration
- Integrate with provider presets for default endpoint candidates
This feature improves user experience when selecting optimal API endpoints,
especially useful for users with multiple provider options or proxy setups.
* refactor: convert endpoint speed test to modal dialog
- Transform EndpointSpeedTest component into a modal dialog
- Add "Advanced" button next to base URL input to open modal
- Support ESC key and backdrop click to close modal
- Apply Linear design principles: minimal styling, clean layout
- Remove unused showBaseUrlInput variable
- Implement same modal pattern for both Claude and Codex
* fix: prevent modal cascade closing when ESC is pressed
- Add state checks to prevent parent modal from closing when child modals (endpoint speed test or template wizard) are open
- Update ESC key handler dependencies to track all modal states
- Ensures only the topmost modal responds to ESC key
* refactor: unify speed test panel UI with project design system
UI improvements:
- Update modal border radius from rounded-lg to rounded-xl
- Unify header padding from px-6 py-4 to p-6
- Change speed test button color to blue theme (bg-blue-500) for consistency
- Update footer background from bg-gray-50 to bg-gray-100
- Style "Done" button as primary action button with blue theme
- Adjust footer button spacing and hover states
Simplify endpoint display:
- Remove endpoint labels (e.g., "Current Address", "Custom 1")
- Display only URL for cleaner interface
- Clean up all label-related logic:
* Remove label field from EndpointCandidate interface
* Remove label generation in buildInitialEntries function
* Remove label handling in useEffect merge logic
* Remove label generation in handleAddEndpoint
* Remove label parameters from claudeSpeedTestEndpoints
* Remove label parameters from codexSpeedTestEndpoints
* refactor: improve endpoint list UI consistency
- Show delete button for all endpoints on hover for uniform UI
- Change selected state to use blue theme matching main interface:
* Blue border (border-blue-500) for selected items
* Light blue background (bg-blue-50/dark:bg-blue-900/20)
* Blue indicator dot (bg-blue-500/dark:bg-blue-400)
- Switch from compact list (space-y-px) to card-based layout (space-y-2)
- Add rounded corners to each endpoint item for better visual separation
* feat: persist custom endpoints to settings.json
- Extend AppSettings to store custom endpoints for Claude and Codex
- Add Tauri commands: get/add/remove/update custom endpoints
- Update frontend API with endpoint persistence methods
- Modify EndpointSpeedTest to load/save custom endpoints via API
- Track endpoint last used time for future sorting/cleanup
- Store endpoints per app type in settings.json instead of localStorage
* - feat(types): add Provider.meta and ProviderMeta (snake_case) with custom_endpoints map
- feat(provider-form): persist custom endpoints on provider create by merging EndpointSpeedTest’s custom URLs into meta.custom_endpoints on submit
- feat(endpoint-speed-test): add onCustomEndpointsChange callback emitting normalized custom URLs; wire it for both Claude/Codex modals
- fix(api): send alias param names (app/appType/app_type and provider_id/providerId) in Tauri invokes to avoid “missing providerId” with older backends
- storage: custom endpoints are stored in ~/.cc-switch/config.json under providers[<id>].meta.custom_endpoints (not in settings.json)
- behavior: edit flow remains immediate writes; create flow now writes once via addProvider, removing the providerId dependency during creation
* feat: add endpoint candidates support and code formatting improvements
- Add endpointCandidates field to ProviderPreset and CodexProviderPreset interfaces
- Integrate preset endpoint candidates into speed test endpoint selection
- Add multiple endpoint options for PackyCode providers (Claude & Codex)
- Apply consistent code formatting (trailing commas, line breaks)
- Improve template value type safety and readability
* refactor: improve endpoint management button UX
Replace ambiguous "Advanced" text with intuitive "Manage & Test" label accompanied by Zap icon, making the endpoint management panel entry point more discoverable and self-explanatory for both Claude and Codex configurations.
* - merge: merge origin/main, resolve conflicts and preserve both feature sets
- feat(tauri): register import/export and file dialogs; keep endpoint speed test and custom endpoints
- feat(api): add updateTrayMenu and onProviderSwitched; wire import/export APIs
- feat(types): extend global API declarations (import/export)
- chore(presets): GLM preset supports both new and legacy model keys
- chore(rust): add chrono dependency; refresh lockfile
---------
Co-authored-by: Jason <farion1231@gmail.com>
2025-10-07 19:14:32 +08:00
|
|
|
|
{latency}ms
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : isTesting ? (
|
|
|
|
|
|
<Loader2 className="h-4 w-4 animate-spin text-gray-400" />
|
|
|
|
|
|
) : entry.error ? (
|
|
|
|
|
|
<div className="text-xs text-gray-400">失败</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="text-xs text-gray-400">—</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
handleRemoveEndpoint(entry);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="opacity-0 transition hover:text-red-600 group-hover:opacity-100 dark:hover:text-red-400"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="h-4 w-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="rounded-md border border-dashed border-gray-200 bg-gray-50 py-8 text-center text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400">
|
|
|
|
|
|
暂无端点
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 错误提示 */}
|
|
|
|
|
|
{lastError && (
|
|
|
|
|
|
<div className="flex items-center gap-1.5 text-xs text-red-600 dark:text-red-400">
|
|
|
|
|
|
<AlertCircle className="h-3 w-3" />
|
|
|
|
|
|
{lastError}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
|
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium"
|
|
|
|
|
|
>
|
|
|
|
|
|
完成
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default EndpointSpeedTest;
|