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
|
|
|
use futures::future::join_all;
|
|
|
|
|
use reqwest::{Client, Url};
|
|
|
|
|
use serde::Serialize;
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
const DEFAULT_TIMEOUT_SECS: u64 = 8;
|
|
|
|
|
const MAX_TIMEOUT_SECS: u64 = 30;
|
|
|
|
|
const MIN_TIMEOUT_SECS: u64 = 2;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
|
|
|
pub struct EndpointLatency {
|
|
|
|
|
pub url: String,
|
|
|
|
|
pub latency: Option<u128>,
|
|
|
|
|
pub status: Option<u16>,
|
|
|
|
|
pub error: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_client(timeout_secs: u64) -> Result<Client, String> {
|
|
|
|
|
Client::builder()
|
|
|
|
|
.timeout(Duration::from_secs(timeout_secs))
|
|
|
|
|
.redirect(reqwest::redirect::Policy::limited(5))
|
|
|
|
|
.user_agent("cc-switch-speedtest/1.0")
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(|e| format!("创建 HTTP 客户端失败: {e}"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn sanitize_timeout(timeout_secs: Option<u64>) -> u64 {
|
|
|
|
|
let secs = timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
|
|
|
|
|
secs.clamp(MIN_TIMEOUT_SECS, MAX_TIMEOUT_SECS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn test_endpoints(
|
|
|
|
|
urls: Vec<String>,
|
|
|
|
|
timeout_secs: Option<u64>,
|
|
|
|
|
) -> Result<Vec<EndpointLatency>, String> {
|
|
|
|
|
if urls.is_empty() {
|
|
|
|
|
return Ok(vec![]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let timeout = sanitize_timeout(timeout_secs);
|
|
|
|
|
let client = build_client(timeout)?;
|
|
|
|
|
|
|
|
|
|
let tasks = urls.into_iter().map(|raw_url| {
|
|
|
|
|
let client = client.clone();
|
|
|
|
|
async move {
|
|
|
|
|
let trimmed = raw_url.trim().to_string();
|
|
|
|
|
if trimmed.is_empty() {
|
|
|
|
|
return EndpointLatency {
|
|
|
|
|
url: raw_url,
|
|
|
|
|
latency: None,
|
|
|
|
|
status: None,
|
|
|
|
|
error: Some("URL 不能为空".to_string()),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let parsed_url = match Url::parse(&trimmed) {
|
|
|
|
|
Ok(url) => url,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
return EndpointLatency {
|
|
|
|
|
url: trimmed,
|
|
|
|
|
latency: None,
|
|
|
|
|
status: None,
|
|
|
|
|
error: Some(format!("URL 无效: {err}")),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-07 20:51:12 +08:00
|
|
|
// 先进行一次“热身”请求,忽略其结果,仅用于复用连接/绕过首包惩罚
|
|
|
|
|
let _ = client.get(parsed_url.clone()).send().await;
|
|
|
|
|
|
|
|
|
|
// 第二次请求开始计时,并将其作为结果返回
|
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
|
|
|
let start = Instant::now();
|
|
|
|
|
match client.get(parsed_url).send().await {
|
|
|
|
|
Ok(resp) => {
|
|
|
|
|
let latency = start.elapsed().as_millis();
|
|
|
|
|
EndpointLatency {
|
|
|
|
|
url: trimmed,
|
|
|
|
|
latency: Some(latency),
|
|
|
|
|
status: Some(resp.status().as_u16()),
|
|
|
|
|
error: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
let status = err.status().map(|s| s.as_u16());
|
|
|
|
|
let error_message = if err.is_timeout() {
|
|
|
|
|
"请求超时".to_string()
|
|
|
|
|
} else if err.is_connect() {
|
|
|
|
|
"连接失败".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
err.to_string()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
EndpointLatency {
|
|
|
|
|
url: trimmed,
|
|
|
|
|
latency: None,
|
|
|
|
|
status,
|
|
|
|
|
error: Some(error_message),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let results = join_all(tasks).await;
|
|
|
|
|
Ok(results)
|
|
|
|
|
}
|