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>
This commit is contained in:
@@ -9,7 +9,8 @@ use crate::app_config::AppType;
|
||||
use crate::claude_plugin;
|
||||
use crate::codex_config;
|
||||
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
||||
use crate::provider::Provider;
|
||||
use crate::provider::{Provider, ProviderMeta};
|
||||
use crate::speedtest;
|
||||
use crate::store::AppState;
|
||||
|
||||
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> {
|
||||
@@ -725,3 +726,191 @@ pub async fn apply_claude_plugin_config(official: bool) -> Result<bool, String>
|
||||
pub async fn is_claude_plugin_applied() -> Result<bool, String> {
|
||||
claude_plugin::is_claude_config_applied()
|
||||
}
|
||||
|
||||
/// 测试第三方/自定义供应商端点的网络延迟
|
||||
#[tauri::command]
|
||||
pub async fn test_api_endpoints(
|
||||
urls: Vec<String>,
|
||||
timeout_secs: Option<u64>,
|
||||
) -> Result<Vec<speedtest::EndpointLatency>, String> {
|
||||
let filtered: Vec<String> = urls
|
||||
.into_iter()
|
||||
.filter(|url| !url.trim().is_empty())
|
||||
.collect();
|
||||
speedtest::test_endpoints(filtered, timeout_secs).await
|
||||
}
|
||||
|
||||
/// 获取自定义端点列表
|
||||
#[tauri::command]
|
||||
pub async fn get_custom_endpoints(
|
||||
state: State<'_, crate::store::AppState>,
|
||||
app_type: Option<AppType>,
|
||||
app: Option<String>,
|
||||
appType: Option<String>,
|
||||
provider_id: Option<String>,
|
||||
providerId: Option<String>,
|
||||
) -> Result<Vec<crate::settings::CustomEndpoint>, String> {
|
||||
let app_type = app_type
|
||||
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||
.unwrap_or(AppType::Claude);
|
||||
let provider_id = provider_id
|
||||
.or(providerId)
|
||||
.ok_or_else(|| "缺少 providerId".to_string())?;
|
||||
let mut cfg_guard = state
|
||||
.config
|
||||
.lock()
|
||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||
|
||||
let manager = cfg_guard
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||
|
||||
let Some(provider) = manager.providers.get_mut(&provider_id) else {
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
// 首选从 provider.meta 读取
|
||||
let meta = provider.meta.get_or_insert_with(ProviderMeta::default);
|
||||
if !meta.custom_endpoints.is_empty() {
|
||||
let mut result: Vec<_> = meta.custom_endpoints.values().cloned().collect();
|
||||
result.sort_by(|a, b| b.added_at.cmp(&a.added_at));
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// 添加自定义端点
|
||||
#[tauri::command]
|
||||
pub async fn add_custom_endpoint(
|
||||
state: State<'_, crate::store::AppState>,
|
||||
app_type: Option<AppType>,
|
||||
app: Option<String>,
|
||||
appType: Option<String>,
|
||||
provider_id: Option<String>,
|
||||
providerId: Option<String>,
|
||||
url: String,
|
||||
) -> Result<(), String> {
|
||||
let app_type = app_type
|
||||
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||
.unwrap_or(AppType::Claude);
|
||||
let provider_id = provider_id
|
||||
.or(providerId)
|
||||
.ok_or_else(|| "缺少 providerId".to_string())?;
|
||||
let normalized = url.trim().trim_end_matches('/').to_string();
|
||||
if normalized.is_empty() {
|
||||
return Err("URL 不能为空".to_string());
|
||||
}
|
||||
|
||||
let mut cfg_guard = state
|
||||
.config
|
||||
.lock()
|
||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||
let manager = cfg_guard
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||
|
||||
let Some(provider) = manager.providers.get_mut(&provider_id) else {
|
||||
return Err("供应商不存在或未选择".to_string());
|
||||
};
|
||||
let meta = provider.meta.get_or_insert_with(ProviderMeta::default);
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as i64;
|
||||
|
||||
let endpoint = crate::settings::CustomEndpoint {
|
||||
url: normalized.clone(),
|
||||
added_at: timestamp,
|
||||
last_used: None,
|
||||
};
|
||||
meta.custom_endpoints.insert(normalized, endpoint);
|
||||
drop(cfg_guard);
|
||||
state.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除自定义端点
|
||||
#[tauri::command]
|
||||
pub async fn remove_custom_endpoint(
|
||||
state: State<'_, crate::store::AppState>,
|
||||
app_type: Option<AppType>,
|
||||
app: Option<String>,
|
||||
appType: Option<String>,
|
||||
provider_id: Option<String>,
|
||||
providerId: Option<String>,
|
||||
url: String,
|
||||
) -> Result<(), String> {
|
||||
let app_type = app_type
|
||||
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||
.unwrap_or(AppType::Claude);
|
||||
let provider_id = provider_id
|
||||
.or(providerId)
|
||||
.ok_or_else(|| "缺少 providerId".to_string())?;
|
||||
let normalized = url.trim().trim_end_matches('/').to_string();
|
||||
|
||||
let mut cfg_guard = state
|
||||
.config
|
||||
.lock()
|
||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||
let manager = cfg_guard
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||
|
||||
if let Some(provider) = manager.providers.get_mut(&provider_id) {
|
||||
if let Some(meta) = provider.meta.as_mut() {
|
||||
meta.custom_endpoints.remove(&normalized);
|
||||
}
|
||||
}
|
||||
drop(cfg_guard);
|
||||
state.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新端点最后使用时间
|
||||
#[tauri::command]
|
||||
pub async fn update_endpoint_last_used(
|
||||
state: State<'_, crate::store::AppState>,
|
||||
app_type: Option<AppType>,
|
||||
app: Option<String>,
|
||||
appType: Option<String>,
|
||||
provider_id: Option<String>,
|
||||
providerId: Option<String>,
|
||||
url: String,
|
||||
) -> Result<(), String> {
|
||||
let app_type = app_type
|
||||
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||
.unwrap_or(AppType::Claude);
|
||||
let provider_id = provider_id
|
||||
.or(providerId)
|
||||
.ok_or_else(|| "缺少 providerId".to_string())?;
|
||||
let normalized = url.trim().trim_end_matches('/').to_string();
|
||||
|
||||
let mut cfg_guard = state
|
||||
.config
|
||||
.lock()
|
||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||
let manager = cfg_guard
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||
|
||||
if let Some(provider) = manager.providers.get_mut(&provider_id) {
|
||||
if let Some(meta) = provider.meta.as_mut() {
|
||||
if let Some(endpoint) = meta.custom_endpoints.get_mut(&normalized) {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as i64;
|
||||
endpoint.last_used = Some(timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(cfg_guard);
|
||||
state.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user