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.
This commit is contained in:
286
src-tauri/Cargo.lock
generated
286
src-tauri/Cargo.lock
generated
@@ -4,9 +4,9 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.24.2"
|
version = "0.25.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gimli",
|
"gimli",
|
||||||
]
|
]
|
||||||
@@ -173,7 +173,7 @@ dependencies = [
|
|||||||
"polling",
|
"polling",
|
||||||
"rustix",
|
"rustix",
|
||||||
"slab",
|
"slab",
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -231,7 +231,7 @@ dependencies = [
|
|||||||
"rustix",
|
"rustix",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"slab",
|
"slab",
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -288,9 +288,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.75"
|
version = "0.3.76"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
|
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"addr2line",
|
"addr2line",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -298,7 +298,7 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
"object",
|
"object",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
"windows-targets 0.52.6",
|
"windows-link 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -465,9 +465,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.23.2"
|
version = "1.24.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
|
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -511,9 +511,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "camino"
|
name = "camino"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603"
|
checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -538,7 +538,7 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -553,9 +553,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.38"
|
version = "1.2.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
|
checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -566,9 +566,11 @@ name = "cc-switch"
|
|||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
"futures",
|
||||||
"log",
|
"log",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit 0.2.2",
|
"objc2-app-kit 0.2.2",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
@@ -579,6 +581,7 @@ dependencies = [
|
|||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
|
"tokio",
|
||||||
"toml 0.8.2",
|
"toml 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -825,12 +828,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.3"
|
version = "0.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
|
checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -906,7 +909,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.2",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1093,7 +1096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1165,9 +1168,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
|
checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
@@ -1237,6 +1240,21 @@ dependencies = [
|
|||||||
"new_debug_unreachable",
|
"new_debug_unreachable",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -1244,6 +1262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1311,6 +1330,7 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@@ -1480,9 +1500,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.31.1"
|
version = "0.32.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gio"
|
name = "gio"
|
||||||
@@ -1797,7 +1817,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.62.0",
|
"windows-core 0.62.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2063,9 +2083,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.80"
|
version = "0.3.81"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
|
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -2148,9 +2168,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.175"
|
version = "0.2.176"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
@@ -2197,11 +2217,10 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.13"
|
version = "0.4.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2259,9 +2278,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.5"
|
version = "2.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "memoffset"
|
||||||
@@ -2322,7 +2341,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2718,9 +2737,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.7"
|
version = "0.37.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -2770,7 +2789,7 @@ dependencies = [
|
|||||||
"objc2-osa-kit",
|
"objc2-osa-kit",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2806,9 +2825,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.4"
|
version = "0.12.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
@@ -2816,15 +2835,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot_core"
|
name = "parking_lot_core"
|
||||||
version = "0.9.11"
|
version = "0.9.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-targets 0.52.6",
|
"windows-link 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3039,7 +3058,7 @@ dependencies = [
|
|||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3192,7 +3211,7 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
@@ -3213,7 +3232,7 @@ dependencies = [
|
|||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
@@ -3235,9 +3254,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -3398,23 +3417,23 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ref-cast"
|
name = "ref-cast"
|
||||||
version = "1.0.24"
|
version = "1.0.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
|
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ref-cast-impl",
|
"ref-cast-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ref-cast-impl"
|
name = "ref-cast-impl"
|
||||||
version = "1.0.24"
|
version = "1.0.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3423,9 +3442,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.2"
|
version = "1.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -3435,9 +3454,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.10"
|
version = "0.4.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -3615,7 +3634,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3644,9 +3663,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.6"
|
version = "0.103.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb"
|
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -3773,9 +3792,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.226"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@@ -3795,18 +3814,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.226"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.226"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3880,9 +3899,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.14.1"
|
version = "3.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e"
|
checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3891,8 +3910,7 @@ dependencies = [
|
|||||||
"indexmap 2.11.4",
|
"indexmap 2.11.4",
|
||||||
"schemars 0.9.0",
|
"schemars 0.9.0",
|
||||||
"schemars 1.0.4",
|
"schemars 1.0.4",
|
||||||
"serde",
|
"serde_core",
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with_macros",
|
"serde_with_macros",
|
||||||
"time",
|
"time",
|
||||||
@@ -3900,9 +3918,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with_macros"
|
name = "serde_with_macros"
|
||||||
version = "3.14.1"
|
version = "3.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e"
|
checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -4292,7 +4310,7 @@ dependencies = [
|
|||||||
"tauri-runtime",
|
"tauri-runtime",
|
||||||
"tauri-runtime-wry",
|
"tauri-runtime-wry",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"url",
|
"url",
|
||||||
@@ -4345,7 +4363,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -4397,7 +4415,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4418,7 +4436,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"toml 0.9.7",
|
"toml 0.9.7",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@@ -4441,7 +4459,7 @@ dependencies = [
|
|||||||
"swift-rs",
|
"swift-rs",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4461,7 +4479,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows",
|
||||||
"zbus",
|
"zbus",
|
||||||
@@ -4486,7 +4504,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
"zbus",
|
"zbus",
|
||||||
@@ -4516,7 +4534,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
@@ -4542,7 +4560,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
@@ -4606,7 +4624,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"swift-rs",
|
"swift-rs",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"toml 0.9.7",
|
"toml 0.9.7",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern",
|
"urlpattern",
|
||||||
@@ -4626,15 +4644,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.22.0"
|
version = "3.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
|
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4659,11 +4677,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.16"
|
version = "2.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
|
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.16",
|
"thiserror-impl 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4679,9 +4697,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.16"
|
version = "2.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
|
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4761,15 +4779,27 @@ dependencies = [
|
|||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"slab",
|
"slab",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-macros"
|
||||||
version = "0.26.3"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd"
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -4978,7 +5008,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4996,9 +5026,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uds_windows"
|
name = "uds_windows"
|
||||||
@@ -5213,9 +5243,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.103"
|
version = "0.2.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
|
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -5226,9 +5256,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.103"
|
version = "0.2.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
|
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
@@ -5240,9 +5270,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.53"
|
version = "0.4.54"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
|
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -5253,9 +5283,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.103"
|
version = "0.2.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
|
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -5263,9 +5293,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.103"
|
version = "0.2.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
|
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5276,9 +5306,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.103"
|
version = "0.2.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
|
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -5358,9 +5388,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.80"
|
version = "0.3.81"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
|
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -5460,7 +5490,7 @@ version = "0.38.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
|
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"windows",
|
"windows",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
@@ -5487,7 +5517,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5548,9 +5578,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.62.0"
|
version = "0.62.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
|
checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement",
|
||||||
"windows-interface",
|
"windows-interface",
|
||||||
@@ -5572,9 +5602,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.60.0"
|
version = "0.60.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5583,9 +5613,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.59.1"
|
version = "0.59.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5692,14 +5722,14 @@ version = "0.60.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets 0.53.3",
|
"windows-targets 0.53.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.0"
|
version = "0.61.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
|
checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.0",
|
"windows-link 0.2.0",
|
||||||
]
|
]
|
||||||
@@ -5752,11 +5782,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.53.3"
|
version = "0.53.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.1.3",
|
"windows-link 0.2.0",
|
||||||
"windows_aarch64_gnullvm 0.53.0",
|
"windows_aarch64_gnullvm 0.53.0",
|
||||||
"windows_aarch64_msvc 0.53.0",
|
"windows_aarch64_msvc 0.53.0",
|
||||||
"windows_i686_gnu 0.53.0",
|
"windows_i686_gnu 0.53.0",
|
||||||
@@ -5778,9 +5808,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-version"
|
name = "windows-version"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69e061eb0a22b4a1d778ad70f7575ec7845490abb35b08fa320df7895882cacb"
|
checksum = "700dad7c058606087f6fdc1f88da5841e06da40334413c6cd4367b25ef26d24e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link 0.2.0",
|
"windows-link 0.2.0",
|
||||||
]
|
]
|
||||||
@@ -6039,7 +6069,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"soup3",
|
"soup3",
|
||||||
"tao-macros",
|
"tao-macros",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
@@ -6218,9 +6248,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.8.1"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ tauri-plugin-updater = "2"
|
|||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::claude_plugin;
|
|||||||
use crate::codex_config;
|
use crate::codex_config;
|
||||||
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
||||||
use crate::provider::Provider;
|
use crate::provider::Provider;
|
||||||
|
use crate::speedtest;
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
|
|
||||||
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> {
|
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> {
|
||||||
@@ -725,3 +726,16 @@ pub async fn apply_claude_plugin_config(official: bool) -> Result<bool, String>
|
|||||||
pub async fn is_claude_plugin_applied() -> Result<bool, String> {
|
pub async fn is_claude_plugin_applied() -> Result<bool, String> {
|
||||||
claude_plugin::is_claude_config_applied()
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod migration;
|
|||||||
mod provider;
|
mod provider;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod store;
|
mod store;
|
||||||
|
mod speedtest;
|
||||||
|
|
||||||
use store::AppState;
|
use store::AppState;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
@@ -419,6 +420,7 @@ pub fn run() {
|
|||||||
commands::read_claude_plugin_config,
|
commands::read_claude_plugin_config,
|
||||||
commands::apply_claude_plugin_config,
|
commands::apply_claude_plugin_config,
|
||||||
commands::is_claude_plugin_applied,
|
commands::is_claude_plugin_applied,
|
||||||
|
commands::test_api_endpoints,
|
||||||
update_tray_menu,
|
update_tray_menu,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
102
src-tauri/src/speedtest.rs
Normal file
102
src-tauri/src/speedtest.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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}")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import { Provider, ProviderCategory } from "../types";
|
import { Provider, ProviderCategory } from "../types";
|
||||||
import { AppType } from "../lib/tauri-api";
|
import { AppType } from "../lib/tauri-api";
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
hasTomlCommonConfigSnippet,
|
hasTomlCommonConfigSnippet,
|
||||||
validateJsonConfig,
|
validateJsonConfig,
|
||||||
applyTemplateValues,
|
applyTemplateValues,
|
||||||
|
extractCodexBaseUrl,
|
||||||
|
setCodexBaseUrl as setCodexBaseUrlInConfig,
|
||||||
} from "../utils/providerConfigUtils";
|
} from "../utils/providerConfigUtils";
|
||||||
import { providerPresets } from "../config/providerPresets";
|
import { providerPresets } from "../config/providerPresets";
|
||||||
import type { TemplateValueConfig } from "../config/providerPresets";
|
import type { TemplateValueConfig } from "../config/providerPresets";
|
||||||
@@ -26,6 +28,9 @@ import CodexConfigEditor from "./ProviderForm/CodexConfigEditor";
|
|||||||
import KimiModelSelector from "./ProviderForm/KimiModelSelector";
|
import KimiModelSelector from "./ProviderForm/KimiModelSelector";
|
||||||
import { X, AlertCircle, Save } from "lucide-react";
|
import { X, AlertCircle, Save } from "lucide-react";
|
||||||
import { isLinux } from "../lib/platform";
|
import { isLinux } from "../lib/platform";
|
||||||
|
import EndpointSpeedTest, {
|
||||||
|
EndpointCandidate,
|
||||||
|
} from "./ProviderForm/EndpointSpeedTest";
|
||||||
// 分类仅用于控制少量交互(如官方禁用 API Key),不显示介绍组件
|
// 分类仅用于控制少量交互(如官方禁用 API Key),不显示介绍组件
|
||||||
|
|
||||||
type TemplateValueMap = Record<string, TemplateValueConfig>;
|
type TemplateValueMap = Record<string, TemplateValueConfig>;
|
||||||
@@ -211,6 +216,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [codexAuth, setCodexAuthState] = useState("");
|
const [codexAuth, setCodexAuthState] = useState("");
|
||||||
const [codexConfig, setCodexConfigState] = useState("");
|
const [codexConfig, setCodexConfigState] = useState("");
|
||||||
const [codexApiKey, setCodexApiKey] = useState("");
|
const [codexApiKey, setCodexApiKey] = useState("");
|
||||||
|
const [codexBaseUrl, setCodexBaseUrl] = useState("");
|
||||||
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
|
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
@@ -223,8 +229,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setCodexAuthError(validateCodexAuth(value));
|
setCodexAuthError(validateCodexAuth(value));
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCodexConfig = (value: string) => {
|
const setCodexConfig = (value: string | ((prev: string) => string)) => {
|
||||||
setCodexConfigState(value);
|
setCodexConfigState((prev) =>
|
||||||
|
typeof value === "function" ? (value as (input: string) => string)(prev) : value,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCodexCommonConfigSnippet = (value: string) => {
|
const setCodexCommonConfigSnippet = (value: string) => {
|
||||||
@@ -238,6 +246,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
if (typeof config === "object" && config !== null) {
|
if (typeof config === "object" && config !== null) {
|
||||||
setCodexAuth(JSON.stringify(config.auth || {}, null, 2));
|
setCodexAuth(JSON.stringify(config.auth || {}, null, 2));
|
||||||
setCodexConfig(config.config || "");
|
setCodexConfig(config.config || "");
|
||||||
|
const initialBaseUrl = extractCodexBaseUrl(config.config);
|
||||||
|
if (initialBaseUrl) {
|
||||||
|
setCodexBaseUrl(initialBaseUrl);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const auth = config.auth || {};
|
const auth = config.auth || {};
|
||||||
if (auth && typeof auth.OPENAI_API_KEY === "string") {
|
if (auth && typeof auth.OPENAI_API_KEY === "string") {
|
||||||
@@ -292,6 +304,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
});
|
});
|
||||||
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
|
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
|
||||||
const isUpdatingFromCodexCommonConfig = useRef(false);
|
const isUpdatingFromCodexCommonConfig = useRef(false);
|
||||||
|
const isUpdatingBaseUrlRef = useRef(false);
|
||||||
|
const isUpdatingCodexBaseUrlRef = useRef(false);
|
||||||
|
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||||
@@ -436,6 +450,49 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}
|
}
|
||||||
}, [showPresets, isCodex, selectedPreset, selectedCodexPreset]);
|
}, [showPresets, isCodex, selectedPreset, selectedCodexPreset]);
|
||||||
|
|
||||||
|
// 与 JSON 配置保持基础 URL 同步(Claude 第三方/自定义)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCodex) return;
|
||||||
|
const currentCategory = category ?? initialData?.category;
|
||||||
|
if (currentCategory !== "third_party" && currentCategory !== "custom") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isUpdatingBaseUrlRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(formData.settingsConfig || "{}");
|
||||||
|
const envUrl: unknown = config?.env?.ANTHROPIC_BASE_URL;
|
||||||
|
if (typeof envUrl === "string" && envUrl && envUrl !== baseUrl) {
|
||||||
|
setBaseUrl(envUrl.trim());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore JSON parse errors
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isCodex,
|
||||||
|
category,
|
||||||
|
initialData,
|
||||||
|
formData.settingsConfig,
|
||||||
|
baseUrl,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 与 TOML 配置保持基础 URL 同步(Codex 第三方/自定义)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCodex) return;
|
||||||
|
const currentCategory = category ?? initialData?.category;
|
||||||
|
if (currentCategory !== "third_party" && currentCategory !== "custom") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isUpdatingCodexBaseUrlRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const extracted = extractCodexBaseUrl(codexConfig) || "";
|
||||||
|
if (extracted !== codexBaseUrl) {
|
||||||
|
setCodexBaseUrl(extracted);
|
||||||
|
}
|
||||||
|
}, [isCodex, category, initialData, codexConfig, codexBaseUrl]);
|
||||||
|
|
||||||
// 同步本地存储的通用配置片段
|
// 同步本地存储的通用配置片段
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
@@ -721,7 +778,6 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
// 清空 API Key 输入框,让用户重新输入
|
// 清空 API Key 输入框,让用户重新输入
|
||||||
setApiKey("");
|
setApiKey("");
|
||||||
setBaseUrl(""); // 清空基础 URL
|
|
||||||
|
|
||||||
// 同步通用配置状态
|
// 同步通用配置状态
|
||||||
const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet);
|
const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet);
|
||||||
@@ -734,6 +790,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
if (config.env) {
|
if (config.env) {
|
||||||
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
|
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
|
||||||
|
const presetBaseUrl =
|
||||||
|
typeof config.env.ANTHROPIC_BASE_URL === "string"
|
||||||
|
? config.env.ANTHROPIC_BASE_URL
|
||||||
|
: "";
|
||||||
|
setBaseUrl(presetBaseUrl);
|
||||||
|
|
||||||
// 如果是 Kimi 预设,同步 Kimi 模型选择
|
// 如果是 Kimi 预设,同步 Kimi 模型选择
|
||||||
if (preset.name?.includes("Kimi")) {
|
if (preset.name?.includes("Kimi")) {
|
||||||
@@ -745,6 +806,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
setClaudeModel("");
|
setClaudeModel("");
|
||||||
setClaudeSmallFastModel("");
|
setClaudeSmallFastModel("");
|
||||||
|
setBaseUrl("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -791,6 +853,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
||||||
setCodexAuth(authString);
|
setCodexAuth(authString);
|
||||||
setCodexConfig(preset.config || "");
|
setCodexConfig(preset.config || "");
|
||||||
|
const presetBaseUrl = extractCodexBaseUrl(preset.config);
|
||||||
|
if (presetBaseUrl) {
|
||||||
|
setCodexBaseUrl(presetBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -828,6 +894,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setCodexAuth(JSON.stringify(customAuth, null, 2));
|
setCodexAuth(JSON.stringify(customAuth, null, 2));
|
||||||
setCodexConfig(customConfig);
|
setCodexConfig(customConfig);
|
||||||
setCodexApiKey("");
|
setCodexApiKey("");
|
||||||
|
setCodexBaseUrl("https://your-api-endpoint.com/v1");
|
||||||
setCategory("custom");
|
setCategory("custom");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -851,21 +918,42 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
// 处理基础 URL 变化
|
// 处理基础 URL 变化
|
||||||
const handleBaseUrlChange = (url: string) => {
|
const handleBaseUrlChange = (url: string) => {
|
||||||
setBaseUrl(url);
|
const sanitized = url.trim().replace(/\/+$/, "");
|
||||||
|
setBaseUrl(sanitized);
|
||||||
|
isUpdatingBaseUrlRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(formData.settingsConfig || "{}");
|
const config = JSON.parse(formData.settingsConfig || "{}");
|
||||||
if (!config.env) {
|
if (!config.env) {
|
||||||
config.env = {};
|
config.env = {};
|
||||||
}
|
}
|
||||||
config.env.ANTHROPIC_BASE_URL = url.trim();
|
config.env.ANTHROPIC_BASE_URL = sanitized;
|
||||||
|
|
||||||
updateSettingsConfigValue(JSON.stringify(config, null, 2));
|
updateSettingsConfigValue(JSON.stringify(config, null, 2));
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
isUpdatingBaseUrlRef.current = false;
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCodexBaseUrlChange = (url: string) => {
|
||||||
|
const sanitized = url.trim().replace(/\/+$/, "");
|
||||||
|
setCodexBaseUrl(sanitized);
|
||||||
|
|
||||||
|
if (!sanitized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdatingCodexBaseUrlRef.current = true;
|
||||||
|
setCodexConfig((prev) => setCodexBaseUrlInConfig(prev, sanitized));
|
||||||
|
setTimeout(() => {
|
||||||
|
isUpdatingCodexBaseUrlRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
// Codex: 处理 API Key 输入并写回 auth.json
|
// Codex: 处理 API Key 输入并写回 auth.json
|
||||||
const handleCodexApiKeyChange = (key: string) => {
|
const handleCodexApiKeyChange = (key: string) => {
|
||||||
setCodexApiKey(key);
|
setCodexApiKey(key);
|
||||||
@@ -971,6 +1059,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setUseCodexCommonConfig(hasCommon);
|
setUseCodexCommonConfig(hasCommon);
|
||||||
}
|
}
|
||||||
setCodexConfig(value);
|
setCodexConfig(value);
|
||||||
|
if (!isUpdatingCodexBaseUrlRef.current) {
|
||||||
|
const extracted = extractCodexBaseUrl(value) || "";
|
||||||
|
if (extracted !== codexBaseUrl) {
|
||||||
|
setCodexBaseUrl(extracted);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据当前配置决定是否展示 API Key 输入框
|
// 根据当前配置决定是否展示 API Key 输入框
|
||||||
@@ -979,6 +1073,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
selectedPreset !== null ||
|
selectedPreset !== null ||
|
||||||
(!showPresets && hasApiKeyField(formData.settingsConfig));
|
(!showPresets && hasApiKeyField(formData.settingsConfig));
|
||||||
|
|
||||||
|
const normalizedCategory = category ?? initialData?.category;
|
||||||
|
const shouldShowSpeedTest =
|
||||||
|
normalizedCategory === "third_party" || normalizedCategory === "custom";
|
||||||
|
|
||||||
const selectedTemplatePreset =
|
const selectedTemplatePreset =
|
||||||
!isCodex &&
|
!isCodex &&
|
||||||
selectedPreset !== null &&
|
selectedPreset !== null &&
|
||||||
@@ -1020,7 +1118,87 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
||||||
|
|
||||||
// 判断是否显示基础 URL 输入框(仅自定义模式显示)
|
// 判断是否显示基础 URL 输入框(仅自定义模式显示)
|
||||||
const showBaseUrlInput = selectedPreset === -1 && !isCodex;
|
const showBaseUrlInput =
|
||||||
|
!isCodex && shouldShowSpeedTest;
|
||||||
|
|
||||||
|
const claudeSpeedTestEndpoints = useMemo<EndpointCandidate[]>(() => {
|
||||||
|
if (isCodex) return [];
|
||||||
|
const map = new Map<string, EndpointCandidate>();
|
||||||
|
const add = (url?: string, label?: string) => {
|
||||||
|
if (!url) return;
|
||||||
|
const sanitized = url.trim().replace(/\/+$/, "");
|
||||||
|
if (!sanitized || map.has(sanitized)) return;
|
||||||
|
map.set(sanitized, { url: sanitized, label });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (baseUrl) {
|
||||||
|
add(baseUrl, "当前地址");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialData && typeof initialData.settingsConfig === "object") {
|
||||||
|
const envUrl = (initialData.settingsConfig as any)?.env?.ANTHROPIC_BASE_URL;
|
||||||
|
if (typeof envUrl === "string") {
|
||||||
|
add(envUrl, envUrl === baseUrl ? "当前地址" : "历史地址");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedPreset !== null &&
|
||||||
|
selectedPreset >= 0 &&
|
||||||
|
selectedPreset < providerPresets.length
|
||||||
|
) {
|
||||||
|
const preset = providerPresets[selectedPreset];
|
||||||
|
const presetEnv = (preset.settingsConfig as any)?.env?.ANTHROPIC_BASE_URL;
|
||||||
|
if (typeof presetEnv === "string") {
|
||||||
|
add(presetEnv, presetEnv === baseUrl ? "当前地址" : "预设地址");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
}, [isCodex, baseUrl, initialData, selectedPreset]);
|
||||||
|
|
||||||
|
const codexSpeedTestEndpoints = useMemo<EndpointCandidate[]>(() => {
|
||||||
|
if (!isCodex) return [];
|
||||||
|
const map = new Map<string, EndpointCandidate>();
|
||||||
|
const add = (url?: string, label?: string) => {
|
||||||
|
if (!url) return;
|
||||||
|
const sanitized = url.trim().replace(/\/+$/, "");
|
||||||
|
if (!sanitized || map.has(sanitized)) return;
|
||||||
|
map.set(sanitized, { url: sanitized, label });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (codexBaseUrl) {
|
||||||
|
add(codexBaseUrl, "当前地址");
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialCodexConfig =
|
||||||
|
initialData && typeof initialData.settingsConfig?.config === "string"
|
||||||
|
? (initialData.settingsConfig as any).config
|
||||||
|
: "";
|
||||||
|
const existing = extractCodexBaseUrl(initialCodexConfig);
|
||||||
|
if (existing) {
|
||||||
|
add(existing, existing === codexBaseUrl ? "当前地址" : "历史地址");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedCodexPreset !== null &&
|
||||||
|
selectedCodexPreset >= 0 &&
|
||||||
|
selectedCodexPreset < codexProviderPresets.length
|
||||||
|
) {
|
||||||
|
const presetConfig = codexProviderPresets[selectedCodexPreset]?.config;
|
||||||
|
const presetBase = extractCodexBaseUrl(presetConfig);
|
||||||
|
if (presetBase) {
|
||||||
|
add(presetBase, presetBase === codexBaseUrl ? "当前地址" : "预设地址");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
}, [
|
||||||
|
isCodex,
|
||||||
|
codexBaseUrl,
|
||||||
|
initialData,
|
||||||
|
selectedCodexPreset,
|
||||||
|
]);
|
||||||
|
|
||||||
// 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示)
|
// 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示)
|
||||||
const shouldShowApiKeyLink =
|
const shouldShowApiKeyLink =
|
||||||
@@ -1392,6 +1570,15 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isCodex && shouldShowSpeedTest && (
|
||||||
|
<EndpointSpeedTest
|
||||||
|
appType={appType}
|
||||||
|
value={baseUrl}
|
||||||
|
onChange={handleBaseUrlChange}
|
||||||
|
initialEndpoints={claudeSpeedTestEndpoints}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 基础 URL 输入框 - 仅在自定义模式下显示 */}
|
{/* 基础 URL 输入框 - 仅在自定义模式下显示 */}
|
||||||
{!isCodex && showBaseUrlInput && (
|
{!isCodex && showBaseUrlInput && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -1462,6 +1649,15 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isCodex && shouldShowSpeedTest && (
|
||||||
|
<EndpointSpeedTest
|
||||||
|
appType={appType}
|
||||||
|
value={codexBaseUrl}
|
||||||
|
onChange={handleCodexBaseUrlChange}
|
||||||
|
initialEndpoints={codexSpeedTestEndpoints}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Claude 或 Codex 的配置部分 */}
|
{/* Claude 或 Codex 的配置部分 */}
|
||||||
{isCodex ? (
|
{isCodex ? (
|
||||||
<CodexConfigEditor
|
<CodexConfigEditor
|
||||||
|
|||||||
478
src/components/ProviderForm/EndpointSpeedTest.tsx
Normal file
478
src/components/ProviderForm/EndpointSpeedTest.tsx
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
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<string, EndpointEntry>();
|
||||||
|
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<EndpointSpeedTestProps> = ({
|
||||||
|
appType,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
initialEndpoints,
|
||||||
|
visible = true,
|
||||||
|
}) => {
|
||||||
|
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(() => {
|
||||||
|
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) {
|
||||||
|
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 (
|
||||||
|
<div className="mt-4 space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||||
|
节点测速
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
添加多个端点后可一键测速,自动选取延迟最低的地址
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<label className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoSelect}
|
||||||
|
onChange={(event) => setAutoSelect(event.target.checked)}
|
||||||
|
className="rounded border-gray-300 text-blue-500 focus:ring-blue-400"
|
||||||
|
/>
|
||||||
|
自动选择最快节点
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={runSpeedTest}
|
||||||
|
disabled={isTesting || entries.length === 0}
|
||||||
|
className="flex items-center gap-2 rounded-md bg-blue-500 px-3 py-1.5 text-sm text-white transition hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{isTesting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
测速中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
开始测速
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasEndpoints ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
key={entry.id}
|
||||||
|
className={`flex items-start justify-between gap-2 rounded-lg border px-3 py-2 text-sm transition ${
|
||||||
|
isSelected
|
||||||
|
? "border-green-400 bg-green-50 dark:border-green-500 dark:bg-green-900/30"
|
||||||
|
: "border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<label className="flex flex-1 cursor-pointer items-start gap-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="endpoint-speedtest"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => handleSelect(entry.url)}
|
||||||
|
className="mt-1 h-4 w-4 border-gray-300 text-green-500 focus:ring-green-400"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-1 flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-gray-800 dark:text-gray-100">
|
||||||
|
{entry.label || "候选节点"}
|
||||||
|
</span>
|
||||||
|
{isSelected && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
||||||
|
<Check className="h-3 w-3" /> 已选中
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="break-all text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{entry.url}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-xs font-mono">
|
||||||
|
{latency !== null ? (
|
||||||
|
<span className={statusBadge}>{latency} ms</span>
|
||||||
|
) : isTesting ? (
|
||||||
|
<span className="text-gray-400">等待结果</span>
|
||||||
|
) : entry.error ? (
|
||||||
|
<span className="flex items-center gap-1 text-red-500">
|
||||||
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
失败
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">未测速</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{entry.isCustom && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveEndpoint(entry)}
|
||||||
|
className="rounded-md p-1 text-red-500 transition hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/30"
|
||||||
|
title="删除该地址"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-4 text-center text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
||||||
|
暂无可测速的地址,请先添加至少一个请求地址
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={customUrl}
|
||||||
|
placeholder="https://example.com/claude"
|
||||||
|
onChange={(event) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddEndpoint}
|
||||||
|
className="flex items-center gap-1 rounded-md bg-green-500 px-3 py-1.5 text-sm text-white transition hover:bg-green-600"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{addError && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">{addError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lastError && (
|
||||||
|
<p className="text-xs text-red-500">
|
||||||
|
<AlertCircle className="mr-1 inline h-3 w-3 align-middle" />
|
||||||
|
{lastError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EndpointSpeedTest;
|
||||||
@@ -18,6 +18,13 @@ interface ImportResult {
|
|||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EndpointLatencyResult {
|
||||||
|
url: string;
|
||||||
|
latency: number | null;
|
||||||
|
status?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Tauri API 封装,提供统一的全局 API 接口
|
// Tauri API 封装,提供统一的全局 API 接口
|
||||||
export const tauriAPI = {
|
export const tauriAPI = {
|
||||||
// 获取所有供应商
|
// 获取所有供应商
|
||||||
@@ -312,6 +319,22 @@ export const tauriAPI = {
|
|||||||
throw new Error(`检测 Claude 插件配置失败: ${String(error)}`);
|
throw new Error(`检测 Claude 插件配置失败: ${String(error)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 第三方/自定义供应商:批量测试端点延迟
|
||||||
|
testApiEndpoints: async (
|
||||||
|
urls: string[],
|
||||||
|
options?: { timeoutSecs?: number },
|
||||||
|
): Promise<EndpointLatencyResult[]> => {
|
||||||
|
try {
|
||||||
|
return await invoke<EndpointLatencyResult[]>("test_api_endpoints", {
|
||||||
|
urls,
|
||||||
|
timeout_secs: options?.timeoutSecs,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("测速调用失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建全局 API 对象,兼容现有代码
|
// 创建全局 API 对象,兼容现有代码
|
||||||
|
|||||||
@@ -365,3 +365,25 @@ export const getCodexBaseUrl = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 在 Codex 的 TOML 配置文本中写入或更新 base_url 字段
|
||||||
|
export const setCodexBaseUrl = (
|
||||||
|
configText: string,
|
||||||
|
baseUrl: string,
|
||||||
|
): string => {
|
||||||
|
const trimmed = baseUrl.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return configText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedUrl = trimmed.replace(/\s+/g, "").replace(/\/+$/, "");
|
||||||
|
const replacementLine = `base_url = "${normalizedUrl}"`;
|
||||||
|
const pattern = /base_url\s*=\s*(["'])([^"']+)\1/;
|
||||||
|
|
||||||
|
if (pattern.test(configText)) {
|
||||||
|
return configText.replace(pattern, replacementLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = configText && !configText.endsWith("\n") ? `${configText}\n` : configText;
|
||||||
|
return `${prefix}${replacementLine}\n`;
|
||||||
|
};
|
||||||
|
|||||||
9
src/vite-env.d.ts
vendored
9
src/vite-env.d.ts
vendored
@@ -49,6 +49,15 @@ declare global {
|
|||||||
official: boolean;
|
official: boolean;
|
||||||
}) => Promise<boolean>;
|
}) => Promise<boolean>;
|
||||||
isClaudePluginApplied: () => Promise<boolean>;
|
isClaudePluginApplied: () => Promise<boolean>;
|
||||||
|
testApiEndpoints: (
|
||||||
|
urls: string[],
|
||||||
|
options?: { timeoutSecs?: number },
|
||||||
|
) => Promise<Array<{
|
||||||
|
url: string;
|
||||||
|
latency: number | null;
|
||||||
|
status?: number;
|
||||||
|
error?: string;
|
||||||
|
}>>;
|
||||||
};
|
};
|
||||||
platform: {
|
platform: {
|
||||||
isMac: boolean;
|
isMac: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user