Compare commits
16 Commits
v3.4.0
...
yovinchen/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
052029b3b0 | ||
|
|
5dc59dc7f8 | ||
|
|
331e48a530 | ||
|
|
aefc5699a2 | ||
|
|
061aef1c2f | ||
|
|
498920dea6 | ||
|
|
9932b92745 | ||
|
|
b4b176580e | ||
|
|
1c9a9af11c | ||
|
|
3ad11acdb2 | ||
|
|
d7fe4a7165 | ||
|
|
f8c40d591f | ||
|
|
ce593248fc | ||
|
|
4fc76200e8 | ||
|
|
e0908701b4 | ||
|
|
d86994eb7e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ release/
|
|||||||
.npmrc
|
.npmrc
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
/.claude
|
||||||
|
|||||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v22.4.1
|
||||||
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### ✨ Features
|
### ✨ Features
|
||||||
- Enable internationalization via i18next with a Chinese default and English fallback, plus an in-app language switcher
|
- Enable internationalization via i18next with a Chinese default and English fallback, plus an in-app language switcher
|
||||||
- Add Claude plugin sync alongside the existing VS Code integration controls
|
- Add Claude plugin sync while retiring the legacy VS Code integration controls (Codex no longer requires settings.json edits)
|
||||||
- Extend provider presets with optional API key URLs and updated models, including DeepSeek-V3.1-Terminus and Qwen3-Max
|
- Extend provider presets with optional API key URLs and updated models, including DeepSeek-V3.1-Terminus and Qwen3-Max
|
||||||
- Support portable mode launches and enforce a single running instance to avoid conflicts
|
- Support portable mode launches and enforce a single running instance to avoid conflicts
|
||||||
|
|
||||||
@@ -22,13 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### 🐛 Fixes
|
### 🐛 Fixes
|
||||||
- Remove the unnecessary OpenAI auth requirement from third-party provider configurations
|
- Remove the unnecessary OpenAI auth requirement from third-party provider configurations
|
||||||
- Fix layout shifts while switching app types with Claude plugin sync enabled
|
- Fix layout shifts while switching app types with Claude plugin sync enabled
|
||||||
- Align Enable/In Use button states to avoid visual jank across VS Code and Codex views
|
- Align Enable/In Use button states to avoid visual jank across app views
|
||||||
|
|
||||||
## [3.3.0] - 2025-09-22
|
## [3.3.0] - 2025-09-22
|
||||||
|
|
||||||
### ✨ Features
|
### ✨ Features
|
||||||
- Add “Apply to VS Code / Remove from VS Code” actions on provider cards, writing settings for Code/Insiders/VSCodium variants
|
- Add “Apply to VS Code / Remove from VS Code” actions on provider cards, writing settings for Code/Insiders/VSCodium variants *(Removed in 3.4.x)*
|
||||||
- Enable VS Code auto-sync by default with window broadcast and tray hooks so Codex switches sync silently
|
- Enable VS Code auto-sync by default with window broadcast and tray hooks so Codex switches sync silently *(Removed in 3.4.x)*
|
||||||
- Extend the Codex provider wizard with display name, dedicated API key URL, and clearer guidance
|
- Extend the Codex provider wizard with display name, dedicated API key URL, and clearer guidance
|
||||||
- Introduce shared common config snippets with JSON/TOML reuse, validation, and consistent error surfaces
|
- Introduce shared common config snippets with JSON/TOML reuse, validation, and consistent error surfaces
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
> v3.4.0 :新增 i18next 国际化(还有部分未完成)、对新模型(qwen-3-max, GLM-4.6, DeepSeek-V3.2-Exp)的支持、Claude 插件、单实例守护、托盘最小化及安装器优化等。
|
> v3.4.0 :新增 i18next 国际化(还有部分未完成)、对新模型(qwen-3-max, GLM-4.6, DeepSeek-V3.2-Exp)的支持、Claude 插件、单实例守护、托盘最小化及安装器优化等。
|
||||||
|
|
||||||
> v3.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。
|
> v3.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。(该 VS Code 写入功能已在 v3.4.x 停用)
|
||||||
|
|
||||||
> v3.2.0 :全新 UI、macOS系统托盘、内置更新器、原子写入与回滚、改进暗色样式、单一事实源(SSOT)与一次性迁移/归档。
|
> v3.2.0 :全新 UI、macOS系统托盘、内置更新器、原子写入与回滚、改进暗色样式、单一事实源(SSOT)与一次性迁移/归档。
|
||||||
|
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
## 功能特性(v3.4.0)
|
## 功能特性(v3.4.0)
|
||||||
|
|
||||||
- **国际化与语言切换**:内置 i18next,默认显示中文,可在设置中快速切换到英文,界面文文案自动实时刷新。
|
- **国际化与语言切换**:内置 i18next,默认显示中文,可在设置中快速切换到英文,界面文文案自动实时刷新。
|
||||||
- **Claude 插件同步**:在 VS Code 同步按钮旁新增 Claude 插件同步选项,与 Codex 同步互不冲突,切换供应商后立即应用。
|
- **Claude 插件同步**:内置按钮可一键应用或恢复 Claude 插件配置,切换供应商后立即生效。
|
||||||
|
- **VS Code Codex 设置停用**:由于新版 Codex 插件无需修改 `settings.json`,应用不再写入 VS Code 设置,避免潜在冲突。
|
||||||
- **供应商预设扩展**:新增 DeepSeek--V3.2-Exp、Qwen3-Max、GLM-4.6 等最新模型。
|
- **供应商预设扩展**:新增 DeepSeek--V3.2-Exp、Qwen3-Max、GLM-4.6 等最新模型。
|
||||||
- **系统托盘与窗口行为**:窗口关闭可最小化到托盘,macOS 支持托盘模式下隐藏/显示 Dock,托盘切换时同步 Claude/Codex/插件状态。
|
- **系统托盘与窗口行为**:窗口关闭可最小化到托盘,macOS 支持托盘模式下隐藏/显示 Dock,托盘切换时同步 Claude/Codex/插件状态。
|
||||||
- **单实例**:保证同一时间仅运行一个实例,避免多开冲突。
|
- **单实例**:保证同一时间仅运行一个实例,避免多开冲突。
|
||||||
|
|||||||
@@ -5,4 +5,3 @@
|
|||||||
- i18n
|
- i18n
|
||||||
- gemini cli
|
- gemini cli
|
||||||
- homebrew 支持
|
- homebrew 支持
|
||||||
- 自定义 vscode 路径
|
|
||||||
|
|||||||
289
src-tauri/Cargo.lock
generated
289
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",
|
||||||
@@ -565,10 +565,13 @@ dependencies = [
|
|||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"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 +582,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -628,8 +632,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-link 0.2.0",
|
"windows-link 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -825,12 +831,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 +912,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 +1099,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 +1171,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 +1243,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 +1265,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 +1333,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 +1503,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 +1820,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 +2086,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 +2171,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 +2220,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 +2281,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 +2344,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 +2740,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 +2792,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 +2828,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 +2838,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 +3061,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 +3214,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 +3235,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 +3257,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 +3420,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 +3445,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 +3457,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 +3637,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 +3666,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 +3795,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 +3817,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 +3902,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 +3913,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 +3921,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 +4313,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 +4366,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 +4418,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 +4439,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 +4462,7 @@ dependencies = [
|
|||||||
"swift-rs",
|
"swift-rs",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4461,7 +4482,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 +4507,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 +4537,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 +4563,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 +4627,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 +4647,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 +4680,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 +4700,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 +4782,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 +5011,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 +5029,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 +5246,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 +5259,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 +5273,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 +5286,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 +5296,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 +5309,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 +5391,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 +5493,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 +5520,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 +5581,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 +5605,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 +5616,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 +5725,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 +5785,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 +5811,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 +6072,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 +6251,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"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ tauri-build = { version = "2.4.0", features = [] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
chrono = "0.4"
|
||||||
tauri = { version = "2.8.2", features = ["tray-icon"] }
|
tauri = { version = "2.8.2", features = ["tray-icon"] }
|
||||||
tauri-plugin-log = "2"
|
tauri-plugin-log = "2"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
@@ -29,6 +30,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"
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ use crate::app_config::AppType;
|
|||||||
use crate::claude_plugin;
|
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, ProviderMeta};
|
||||||
|
use crate::speedtest;
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
use crate::vscode;
|
|
||||||
|
|
||||||
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> {
|
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> {
|
||||||
match app_type {
|
match app_type {
|
||||||
@@ -693,45 +693,6 @@ pub async fn is_portable_mode() -> Result<bool, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// VS Code: 获取用户 settings.json 状态
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_vscode_settings_status() -> Result<ConfigStatus, String> {
|
|
||||||
if let Some(p) = vscode::find_existing_settings() {
|
|
||||||
Ok(ConfigStatus {
|
|
||||||
exists: true,
|
|
||||||
path: p.to_string_lossy().to_string(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 默认返回 macOS 稳定版路径(或其他平台首选项的第一个候选),但标记不存在
|
|
||||||
let preferred = vscode::candidate_settings_paths().into_iter().next();
|
|
||||||
Ok(ConfigStatus {
|
|
||||||
exists: false,
|
|
||||||
path: preferred.unwrap_or_default().to_string_lossy().to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// VS Code: 读取 settings.json 文本(仅当文件存在)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn read_vscode_settings() -> Result<String, String> {
|
|
||||||
if let Some(p) = vscode::find_existing_settings() {
|
|
||||||
std::fs::read_to_string(&p).map_err(|e| format!("读取 VS Code 设置失败: {}", e))
|
|
||||||
} else {
|
|
||||||
Err("未找到 VS Code 用户设置文件".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// VS Code: 写入 settings.json 文本(仅当文件存在;不自动创建)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn write_vscode_settings(content: String) -> Result<bool, String> {
|
|
||||||
if let Some(p) = vscode::find_existing_settings() {
|
|
||||||
config::write_text_file(&p, &content)?;
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Err("未找到 VS Code 用户设置文件".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Claude 插件:获取 ~/.claude/config.json 状态
|
/// Claude 插件:获取 ~/.claude/config.json 状态
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_claude_plugin_status() -> Result<ConfigStatus, String> {
|
pub async fn get_claude_plugin_status() -> Result<ConfigStatus, String> {
|
||||||
@@ -765,3 +726,191 @@ pub async fn apply_claude_plugin_config(official: bool) -> Result<bool, String>
|
|||||||
pub async fn is_claude_plugin_applied() -> Result<bool, String> {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取自定义端点列表
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_custom_endpoints(
|
||||||
|
state: State<'_, crate::store::AppState>,
|
||||||
|
app_type: Option<AppType>,
|
||||||
|
app: Option<String>,
|
||||||
|
appType: Option<String>,
|
||||||
|
provider_id: Option<String>,
|
||||||
|
providerId: Option<String>,
|
||||||
|
) -> Result<Vec<crate::settings::CustomEndpoint>, String> {
|
||||||
|
let app_type = app_type
|
||||||
|
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||||
|
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||||
|
.unwrap_or(AppType::Claude);
|
||||||
|
let provider_id = provider_id
|
||||||
|
.or(providerId)
|
||||||
|
.ok_or_else(|| "缺少 providerId".to_string())?;
|
||||||
|
let mut cfg_guard = state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
let manager = cfg_guard
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
|
||||||
|
let Some(provider) = manager.providers.get_mut(&provider_id) else {
|
||||||
|
return Ok(vec![]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 首选从 provider.meta 读取
|
||||||
|
let meta = provider.meta.get_or_insert_with(ProviderMeta::default);
|
||||||
|
if !meta.custom_endpoints.is_empty() {
|
||||||
|
let mut result: Vec<_> = meta.custom_endpoints.values().cloned().collect();
|
||||||
|
result.sort_by(|a, b| b.added_at.cmp(&a.added_at));
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加自定义端点
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn add_custom_endpoint(
|
||||||
|
state: State<'_, crate::store::AppState>,
|
||||||
|
app_type: Option<AppType>,
|
||||||
|
app: Option<String>,
|
||||||
|
appType: Option<String>,
|
||||||
|
provider_id: Option<String>,
|
||||||
|
providerId: Option<String>,
|
||||||
|
url: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let app_type = app_type
|
||||||
|
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||||
|
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||||
|
.unwrap_or(AppType::Claude);
|
||||||
|
let provider_id = provider_id
|
||||||
|
.or(providerId)
|
||||||
|
.ok_or_else(|| "缺少 providerId".to_string())?;
|
||||||
|
let normalized = url.trim().trim_end_matches('/').to_string();
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return Err("URL 不能为空".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cfg_guard = state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
let manager = cfg_guard
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
|
||||||
|
let Some(provider) = manager.providers.get_mut(&provider_id) else {
|
||||||
|
return Err("供应商不存在或未选择".to_string());
|
||||||
|
};
|
||||||
|
let meta = provider.meta.get_or_insert_with(ProviderMeta::default);
|
||||||
|
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
let endpoint = crate::settings::CustomEndpoint {
|
||||||
|
url: normalized.clone(),
|
||||||
|
added_at: timestamp,
|
||||||
|
last_used: None,
|
||||||
|
};
|
||||||
|
meta.custom_endpoints.insert(normalized, endpoint);
|
||||||
|
drop(cfg_guard);
|
||||||
|
state.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除自定义端点
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_custom_endpoint(
|
||||||
|
state: State<'_, crate::store::AppState>,
|
||||||
|
app_type: Option<AppType>,
|
||||||
|
app: Option<String>,
|
||||||
|
appType: Option<String>,
|
||||||
|
provider_id: Option<String>,
|
||||||
|
providerId: Option<String>,
|
||||||
|
url: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let app_type = app_type
|
||||||
|
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||||
|
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||||
|
.unwrap_or(AppType::Claude);
|
||||||
|
let provider_id = provider_id
|
||||||
|
.or(providerId)
|
||||||
|
.ok_or_else(|| "缺少 providerId".to_string())?;
|
||||||
|
let normalized = url.trim().trim_end_matches('/').to_string();
|
||||||
|
|
||||||
|
let mut cfg_guard = state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
let manager = cfg_guard
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
|
||||||
|
if let Some(provider) = manager.providers.get_mut(&provider_id) {
|
||||||
|
if let Some(meta) = provider.meta.as_mut() {
|
||||||
|
meta.custom_endpoints.remove(&normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(cfg_guard);
|
||||||
|
state.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新端点最后使用时间
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_endpoint_last_used(
|
||||||
|
state: State<'_, crate::store::AppState>,
|
||||||
|
app_type: Option<AppType>,
|
||||||
|
app: Option<String>,
|
||||||
|
appType: Option<String>,
|
||||||
|
provider_id: Option<String>,
|
||||||
|
providerId: Option<String>,
|
||||||
|
url: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let app_type = app_type
|
||||||
|
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||||
|
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||||
|
.unwrap_or(AppType::Claude);
|
||||||
|
let provider_id = provider_id
|
||||||
|
.or(providerId)
|
||||||
|
.ok_or_else(|| "缺少 providerId".to_string())?;
|
||||||
|
let normalized = url.trim().trim_end_matches('/').to_string();
|
||||||
|
|
||||||
|
let mut cfg_guard = state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
let manager = cfg_guard
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
|
||||||
|
if let Some(provider) = manager.providers.get_mut(&provider_id) {
|
||||||
|
if let Some(meta) = provider.meta.as_mut() {
|
||||||
|
if let Some(endpoint) = meta.custom_endpoints.get_mut(&normalized) {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
endpoint.last_used = Some(timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(cfg_guard);
|
||||||
|
state.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
170
src-tauri/src/import_export.rs
Normal file
170
src-tauri/src/import_export.rs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// 默认仅保留最近 10 份备份,避免目录无限膨胀
|
||||||
|
const MAX_BACKUPS: usize = 10;
|
||||||
|
|
||||||
|
/// 创建配置文件备份
|
||||||
|
pub fn create_backup(config_path: &PathBuf) -> Result<String, String> {
|
||||||
|
if !config_path.exists() {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
|
||||||
|
let backup_id = format!("backup_{}", timestamp);
|
||||||
|
|
||||||
|
let backup_dir = config_path
|
||||||
|
.parent()
|
||||||
|
.ok_or("Invalid config path")?
|
||||||
|
.join("backups");
|
||||||
|
|
||||||
|
// 创建备份目录
|
||||||
|
fs::create_dir_all(&backup_dir)
|
||||||
|
.map_err(|e| format!("Failed to create backup directory: {}", e))?;
|
||||||
|
|
||||||
|
let backup_path = backup_dir.join(format!("{}.json", backup_id));
|
||||||
|
|
||||||
|
// 复制配置文件到备份
|
||||||
|
fs::copy(config_path, backup_path).map_err(|e| format!("Failed to create backup: {}", e))?;
|
||||||
|
|
||||||
|
// 备份完成后清理旧的备份文件(仅保留最近 MAX_BACKUPS 份)
|
||||||
|
cleanup_old_backups(&backup_dir, MAX_BACKUPS)?;
|
||||||
|
|
||||||
|
Ok(backup_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_old_backups(backup_dir: &PathBuf, retain: usize) -> Result<(), String> {
|
||||||
|
if retain == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries: Vec<_> = match fs::read_dir(backup_dir) {
|
||||||
|
Ok(iter) => iter
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.filter(|entry| {
|
||||||
|
entry
|
||||||
|
.path()
|
||||||
|
.extension()
|
||||||
|
.map(|ext| ext == "json")
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
Err(_) => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if entries.len() <= retain {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let remove_count = entries.len().saturating_sub(retain);
|
||||||
|
|
||||||
|
entries.sort_by(|a, b| {
|
||||||
|
let a_time = a.metadata().and_then(|m| m.modified()).ok();
|
||||||
|
let b_time = b.metadata().and_then(|m| m.modified()).ok();
|
||||||
|
a_time.cmp(&b_time)
|
||||||
|
});
|
||||||
|
|
||||||
|
for entry in entries.into_iter().take(remove_count) {
|
||||||
|
if let Err(err) = fs::remove_file(entry.path()) {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to remove old backup {}: {}",
|
||||||
|
entry.path().display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导出配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn export_config_to_file(file_path: String) -> Result<Value, String> {
|
||||||
|
// 读取当前配置文件
|
||||||
|
let config_path = crate::config::get_app_config_path();
|
||||||
|
let config_content = fs::read_to_string(&config_path)
|
||||||
|
.map_err(|e| format!("Failed to read configuration: {}", e))?;
|
||||||
|
|
||||||
|
// 写入到指定文件
|
||||||
|
fs::write(&file_path, &config_content).map_err(|e| format!("Failed to write file: {}", e))?;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "Configuration exported successfully",
|
||||||
|
"filePath": file_path
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从文件导入配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_config_from_file(
|
||||||
|
file_path: String,
|
||||||
|
state: tauri::State<'_, crate::store::AppState>,
|
||||||
|
) -> Result<Value, String> {
|
||||||
|
// 读取导入的文件
|
||||||
|
let import_content =
|
||||||
|
fs::read_to_string(&file_path).map_err(|e| format!("Failed to read import file: {}", e))?;
|
||||||
|
|
||||||
|
// 验证并解析为配置对象
|
||||||
|
let new_config: crate::app_config::MultiAppConfig = serde_json::from_str(&import_content)
|
||||||
|
.map_err(|e| format!("Invalid configuration file: {}", e))?;
|
||||||
|
|
||||||
|
// 备份当前配置
|
||||||
|
let config_path = crate::config::get_app_config_path();
|
||||||
|
let backup_id = create_backup(&config_path)?;
|
||||||
|
|
||||||
|
// 写入新配置到磁盘
|
||||||
|
fs::write(&config_path, &import_content)
|
||||||
|
.map_err(|e| format!("Failed to write configuration: {}", e))?;
|
||||||
|
|
||||||
|
// 更新内存中的状态
|
||||||
|
{
|
||||||
|
let mut config_state = state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("Failed to lock config: {}", e))?;
|
||||||
|
*config_state = new_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "Configuration imported successfully",
|
||||||
|
"backupId": backup_id
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存文件对话框
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_file_dialog<R: tauri::Runtime>(
|
||||||
|
app: tauri::AppHandle<R>,
|
||||||
|
default_name: String,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
|
||||||
|
let dialog = app.dialog();
|
||||||
|
let result = dialog
|
||||||
|
.file()
|
||||||
|
.add_filter("JSON", &["json"])
|
||||||
|
.set_file_name(&default_name)
|
||||||
|
.blocking_save_file();
|
||||||
|
|
||||||
|
Ok(result.map(|p| p.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开文件对话框
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_file_dialog<R: tauri::Runtime>(
|
||||||
|
app: tauri::AppHandle<R>,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
|
||||||
|
let dialog = app.dialog();
|
||||||
|
let result = dialog
|
||||||
|
.file()
|
||||||
|
.add_filter("JSON", &["json"])
|
||||||
|
.blocking_pick_file();
|
||||||
|
|
||||||
|
Ok(result.map(|p| p.to_string()))
|
||||||
|
}
|
||||||
@@ -3,11 +3,12 @@ mod claude_plugin;
|
|||||||
mod codex_config;
|
mod codex_config;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod import_export;
|
||||||
mod migration;
|
mod migration;
|
||||||
mod provider;
|
mod provider;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod store;
|
mod store;
|
||||||
mod vscode;
|
mod speedtest;
|
||||||
|
|
||||||
use store::AppState;
|
use store::AppState;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
@@ -416,13 +417,21 @@ pub fn run() {
|
|||||||
commands::save_settings,
|
commands::save_settings,
|
||||||
commands::check_for_updates,
|
commands::check_for_updates,
|
||||||
commands::is_portable_mode,
|
commands::is_portable_mode,
|
||||||
commands::get_vscode_settings_status,
|
|
||||||
commands::read_vscode_settings,
|
|
||||||
commands::write_vscode_settings,
|
|
||||||
commands::get_claude_plugin_status,
|
commands::get_claude_plugin_status,
|
||||||
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,
|
||||||
|
// ours: endpoint speed test + custom endpoint management
|
||||||
|
commands::test_api_endpoints,
|
||||||
|
commands::get_custom_endpoints,
|
||||||
|
commands::add_custom_endpoint,
|
||||||
|
commands::remove_custom_endpoint,
|
||||||
|
commands::update_endpoint_last_used,
|
||||||
|
// theirs: config import/export and dialogs
|
||||||
|
import_export::export_config_to_file,
|
||||||
|
import_export::import_config_from_file,
|
||||||
|
import_export::save_file_dialog,
|
||||||
|
import_export::open_file_dialog,
|
||||||
update_tray_menu,
|
update_tray_menu,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ pub struct Provider {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(rename = "createdAt")]
|
#[serde(rename = "createdAt")]
|
||||||
pub created_at: Option<i64>,
|
pub created_at: Option<i64>,
|
||||||
|
/// 供应商元数据(不写入 live 配置,仅存于 ~/.cc-switch/config.json)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub meta: Option<ProviderMeta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Provider {
|
impl Provider {
|
||||||
@@ -36,6 +39,7 @@ impl Provider {
|
|||||||
website_url,
|
website_url,
|
||||||
category: None,
|
category: None,
|
||||||
created_at: None,
|
created_at: None,
|
||||||
|
meta: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +60,14 @@ impl Default for ProviderManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 供应商元数据
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct ProviderMeta {
|
||||||
|
/// 自定义端点列表(按 URL 去重存储)
|
||||||
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
|
pub custom_endpoints: HashMap<String, crate::settings::CustomEndpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ProviderManager {
|
impl ProviderManager {
|
||||||
/// 获取所有供应商
|
/// 获取所有供应商
|
||||||
pub fn get_all_providers(&self) -> &HashMap<String, Provider> {
|
pub fn get_all_providers(&self) -> &HashMap<String, Provider> {
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{OnceLock, RwLock};
|
use std::sync::{OnceLock, RwLock};
|
||||||
|
|
||||||
|
/// 自定义端点配置
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CustomEndpoint {
|
||||||
|
pub url: String,
|
||||||
|
pub added_at: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_used: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
/// 应用设置结构,允许覆盖默认配置目录
|
/// 应用设置结构,允许覆盖默认配置目录
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -17,6 +28,12 @@ pub struct AppSettings {
|
|||||||
pub codex_config_dir: Option<String>,
|
pub codex_config_dir: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub language: Option<String>,
|
pub language: Option<String>,
|
||||||
|
/// Claude 自定义端点列表
|
||||||
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
|
pub custom_endpoints_claude: HashMap<String, CustomEndpoint>,
|
||||||
|
/// Codex 自定义端点列表
|
||||||
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
|
pub custom_endpoints_codex: HashMap<String, CustomEndpoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_show_in_tray() -> bool {
|
fn default_show_in_tray() -> bool {
|
||||||
@@ -35,6 +52,8 @@ impl Default for AppSettings {
|
|||||||
claude_config_dir: None,
|
claude_config_dir: None,
|
||||||
codex_config_dir: None,
|
codex_config_dir: None,
|
||||||
language: None,
|
language: None,
|
||||||
|
custom_endpoints_claude: HashMap::new(),
|
||||||
|
custom_endpoints_codex: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,65 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// 枚举可能的 VS Code 发行版配置目录名称
|
|
||||||
fn vscode_product_dirs() -> Vec<&'static str> {
|
|
||||||
vec![
|
|
||||||
"Code", // VS Code Stable
|
|
||||||
"Code - Insiders", // VS Code Insiders
|
|
||||||
"VSCodium", // VSCodium
|
|
||||||
"Code - OSS", // OSS 发行版
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取 VS Code 用户 settings.json 的候选路径列表(按优先级排序)
|
|
||||||
pub fn candidate_settings_paths() -> Vec<PathBuf> {
|
|
||||||
let mut paths = Vec::new();
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
if let Some(home) = dirs::home_dir() {
|
|
||||||
for prod in vscode_product_dirs() {
|
|
||||||
paths.push(
|
|
||||||
home.join("Library")
|
|
||||||
.join("Application Support")
|
|
||||||
.join(prod)
|
|
||||||
.join("User")
|
|
||||||
.join("settings.json"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
// Windows: %APPDATA%\Code\User\settings.json
|
|
||||||
if let Some(roaming) = dirs::config_dir() {
|
|
||||||
for prod in vscode_product_dirs() {
|
|
||||||
paths.push(roaming.join(prod).join("User").join("settings.json"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
|
||||||
{
|
|
||||||
// Linux: ~/.config/Code/User/settings.json
|
|
||||||
if let Some(config) = dirs::config_dir() {
|
|
||||||
for prod in vscode_product_dirs() {
|
|
||||||
paths.push(config.join(prod).join("User").join("settings.json"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
paths
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 返回第一个存在的 settings.json 路径
|
|
||||||
pub fn find_existing_settings() -> Option<PathBuf> {
|
|
||||||
for p in candidate_settings_paths() {
|
|
||||||
if let Ok(meta) = std::fs::metadata(&p) {
|
|
||||||
if meta.is_file() {
|
|
||||||
return Some(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
86
src/App.tsx
86
src/App.tsx
@@ -13,14 +13,10 @@ import { Plus, Settings, Moon, Sun } from "lucide-react";
|
|||||||
import { buttonStyles } from "./lib/styles";
|
import { buttonStyles } from "./lib/styles";
|
||||||
import { useDarkMode } from "./hooks/useDarkMode";
|
import { useDarkMode } from "./hooks/useDarkMode";
|
||||||
import { extractErrorMessage } from "./utils/errorUtils";
|
import { extractErrorMessage } from "./utils/errorUtils";
|
||||||
import { applyProviderToVSCode } from "./utils/vscodeSettings";
|
|
||||||
import { getCodexBaseUrl } from "./utils/providerConfigUtils";
|
|
||||||
import { useVSCodeAutoSync } from "./hooks/useVSCodeAutoSync";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||||
const { isAutoSyncEnabled } = useVSCodeAutoSync();
|
|
||||||
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
||||||
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
||||||
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
||||||
@@ -98,11 +94,7 @@ function App() {
|
|||||||
await loadProviders();
|
await loadProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 若为 Codex 且开启自动同步,则静默同步到 VS Code(覆盖)
|
// 若为 Claude,则同步插件配置
|
||||||
if (data.appType === "codex" && isAutoSyncEnabled) {
|
|
||||||
await syncCodexToVSCode(data.providerId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.appType === "claude") {
|
if (data.appType === "claude") {
|
||||||
await syncClaudePlugin(data.providerId, true);
|
await syncClaudePlugin(data.providerId, true);
|
||||||
}
|
}
|
||||||
@@ -120,7 +112,7 @@ function App() {
|
|||||||
unlisten();
|
unlisten();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [activeApp, isAutoSyncEnabled]);
|
}, [activeApp]);
|
||||||
|
|
||||||
const loadProviders = async () => {
|
const loadProviders = async () => {
|
||||||
const loadedProviders = await window.api.getProviders(activeApp);
|
const loadedProviders = await window.api.getProviders(activeApp);
|
||||||
@@ -189,61 +181,6 @@ function App() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同步Codex供应商到VS Code设置(静默覆盖)
|
|
||||||
const syncCodexToVSCode = async (providerId: string, silent = false) => {
|
|
||||||
try {
|
|
||||||
const status = await window.api.getVSCodeSettingsStatus();
|
|
||||||
if (!status.exists) {
|
|
||||||
if (!silent) {
|
|
||||||
showNotification(
|
|
||||||
t("notifications.vscodeSettingsNotFound"),
|
|
||||||
"error",
|
|
||||||
3000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = await window.api.readVSCodeSettings();
|
|
||||||
const provider = providers[providerId];
|
|
||||||
const isOfficial = provider?.category === "official";
|
|
||||||
|
|
||||||
// 非官方供应商需要解析 base_url(使用公共工具函数)
|
|
||||||
let baseUrl: string | undefined = undefined;
|
|
||||||
if (!isOfficial) {
|
|
||||||
const parsed = getCodexBaseUrl(provider);
|
|
||||||
if (!parsed) {
|
|
||||||
if (!silent) {
|
|
||||||
showNotification(t("notifications.missingBaseUrl"), "error", 4000);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
baseUrl = parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedSettings = applyProviderToVSCode(raw, {
|
|
||||||
baseUrl,
|
|
||||||
isOfficial,
|
|
||||||
});
|
|
||||||
if (updatedSettings !== raw) {
|
|
||||||
await window.api.writeVSCodeSettings(updatedSettings);
|
|
||||||
if (!silent) {
|
|
||||||
showNotification(t("notifications.syncedToVSCode"), "success", 1500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发providers重新加载,以更新VS Code按钮状态
|
|
||||||
await loadProviders();
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(t("console.syncToVSCodeFailed"), error);
|
|
||||||
if (!silent) {
|
|
||||||
const errorMessage =
|
|
||||||
error?.message || t("notifications.syncVSCodeFailed");
|
|
||||||
showNotification(errorMessage, "error", 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 同步 Claude 插件配置(写入/移除固定 JSON)
|
// 同步 Claude 插件配置(写入/移除固定 JSON)
|
||||||
const syncClaudePlugin = async (providerId: string, silent = false) => {
|
const syncClaudePlugin = async (providerId: string, silent = false) => {
|
||||||
try {
|
try {
|
||||||
@@ -284,11 +221,6 @@ function App() {
|
|||||||
// 更新托盘菜单
|
// 更新托盘菜单
|
||||||
await window.api.updateTrayMenu();
|
await window.api.updateTrayMenu();
|
||||||
|
|
||||||
// Codex: 切换供应商后,只在自动同步启用时同步到 VS Code
|
|
||||||
if (activeApp === "codex" && isAutoSyncEnabled) {
|
|
||||||
await syncCodexToVSCode(id, true); // silent模式,不显示通知
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeApp === "claude") {
|
if (activeApp === "claude") {
|
||||||
await syncClaudePlugin(id, true);
|
await syncClaudePlugin(id, true);
|
||||||
}
|
}
|
||||||
@@ -297,6 +229,15 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportSuccess = async () => {
|
||||||
|
await loadProviders();
|
||||||
|
try {
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to refresh tray menu after import", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 自动从 live 导入一条默认供应商(仅首次初始化时)
|
// 自动从 live 导入一条默认供应商(仅首次初始化时)
|
||||||
const handleAutoImportDefault = async () => {
|
const handleAutoImportDefault = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -425,7 +366,10 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isSettingsOpen && (
|
{isSettingsOpen && (
|
||||||
<SettingsModal onClose={() => setIsSettingsOpen(false)} />
|
<SettingsModal
|
||||||
|
onClose={() => setIsSettingsOpen(false)}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
103
src/components/ImportProgressModal.tsx
Normal file
103
src/components/ImportProgressModal.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { CheckCircle, Loader2, AlertCircle } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface ImportProgressModalProps {
|
||||||
|
status: 'importing' | 'success' | 'error';
|
||||||
|
message?: string;
|
||||||
|
backupId?: string;
|
||||||
|
onComplete?: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportProgressModal({
|
||||||
|
status,
|
||||||
|
message,
|
||||||
|
backupId,
|
||||||
|
onComplete,
|
||||||
|
onSuccess
|
||||||
|
}: ImportProgressModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'success') {
|
||||||
|
console.log('[ImportProgressModal] Success detected, starting 2 second countdown');
|
||||||
|
// 成功后等待2秒自动关闭并刷新数据
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
console.log('[ImportProgressModal] 2 seconds elapsed, calling callbacks...');
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log('[ImportProgressModal] Cleanup timer');
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [status, onComplete, onSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||||
|
<div className="absolute inset-0 bg-black/50 dark:bg-black/70 backdrop-blur-sm" />
|
||||||
|
|
||||||
|
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-2xl p-8 max-w-md w-full mx-4">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
{status === 'importing' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
{t("settings.importing")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{t("common.loading")}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'success' && (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-12 h-12 text-green-500 mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
{t("settings.importSuccess")}
|
||||||
|
</h3>
|
||||||
|
{backupId && (
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
{t("settings.backupId")}: {backupId}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{t("settings.autoReload")}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'error' && (
|
||||||
|
<>
|
||||||
|
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
{t("settings.importFailed")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{message || t("settings.configCorrupted")}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{t("common.close")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import { Provider, ProviderCategory } from "../types";
|
import { Provider, ProviderCategory, CustomEndpoint } from "../types";
|
||||||
import { AppType } from "../lib/tauri-api";
|
import { AppType } from "../lib/tauri-api";
|
||||||
import {
|
import {
|
||||||
updateCommonConfigSnippet,
|
updateCommonConfigSnippet,
|
||||||
@@ -10,8 +10,12 @@ import {
|
|||||||
updateTomlCommonConfigSnippet,
|
updateTomlCommonConfigSnippet,
|
||||||
hasTomlCommonConfigSnippet,
|
hasTomlCommonConfigSnippet,
|
||||||
validateJsonConfig,
|
validateJsonConfig,
|
||||||
|
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 {
|
import {
|
||||||
codexProviderPresets,
|
codexProviderPresets,
|
||||||
generateThirdPartyAuth,
|
generateThirdPartyAuth,
|
||||||
@@ -22,10 +26,143 @@ import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
|||||||
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
||||||
import CodexConfigEditor from "./ProviderForm/CodexConfigEditor";
|
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, Zap } 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 TemplatePath = Array<string | number>;
|
||||||
|
|
||||||
|
const collectTemplatePaths = (
|
||||||
|
source: unknown,
|
||||||
|
templateKeys: string[],
|
||||||
|
currentPath: TemplatePath = [],
|
||||||
|
acc: TemplatePath[] = []
|
||||||
|
): TemplatePath[] => {
|
||||||
|
if (typeof source === "string") {
|
||||||
|
const hasPlaceholder = templateKeys.some((key) =>
|
||||||
|
source.includes(`\${${key}}`)
|
||||||
|
);
|
||||||
|
if (hasPlaceholder) {
|
||||||
|
acc.push([...currentPath]);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(source)) {
|
||||||
|
source.forEach((item, index) =>
|
||||||
|
collectTemplatePaths(item, templateKeys, [...currentPath, index], acc)
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source && typeof source === "object") {
|
||||||
|
Object.entries(source).forEach(([key, value]) =>
|
||||||
|
collectTemplatePaths(value, templateKeys, [...currentPath, key], acc)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValueAtPath = (source: any, path: TemplatePath) => {
|
||||||
|
return path.reduce<any>((acc, key) => {
|
||||||
|
if (acc === undefined || acc === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return acc[key as keyof typeof acc];
|
||||||
|
}, source);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValueAtPath = (
|
||||||
|
target: any,
|
||||||
|
path: TemplatePath,
|
||||||
|
value: unknown
|
||||||
|
): any => {
|
||||||
|
if (path.length === 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = target;
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
const key = path[i];
|
||||||
|
const nextKey = path[i + 1];
|
||||||
|
const isNextIndex = typeof nextKey === "number";
|
||||||
|
|
||||||
|
if (current[key as keyof typeof current] === undefined) {
|
||||||
|
current[key as keyof typeof current] = isNextIndex ? [] : {};
|
||||||
|
} else {
|
||||||
|
const currentValue = current[key as keyof typeof current];
|
||||||
|
if (isNextIndex && !Array.isArray(currentValue)) {
|
||||||
|
current[key as keyof typeof current] = [];
|
||||||
|
} else if (
|
||||||
|
!isNextIndex &&
|
||||||
|
(typeof currentValue !== "object" || currentValue === null)
|
||||||
|
) {
|
||||||
|
current[key as keyof typeof current] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current[key as keyof typeof current];
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalKey = path[path.length - 1];
|
||||||
|
current[finalKey as keyof typeof current] = value;
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyTemplateValuesToConfigString = (
|
||||||
|
presetConfig: any,
|
||||||
|
currentConfigString: string,
|
||||||
|
values: TemplateValueMap
|
||||||
|
) => {
|
||||||
|
const replacedConfig = applyTemplateValues(presetConfig, values);
|
||||||
|
const templateKeys = Object.keys(values);
|
||||||
|
if (templateKeys.length === 0) {
|
||||||
|
return JSON.stringify(replacedConfig, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderPaths = collectTemplatePaths(presetConfig, templateKeys);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedConfig = currentConfigString.trim()
|
||||||
|
? JSON.parse(currentConfigString)
|
||||||
|
: {};
|
||||||
|
let targetConfig: any;
|
||||||
|
if (Array.isArray(parsedConfig)) {
|
||||||
|
targetConfig = [...parsedConfig];
|
||||||
|
} else if (parsedConfig && typeof parsedConfig === "object") {
|
||||||
|
targetConfig = JSON.parse(JSON.stringify(parsedConfig));
|
||||||
|
} else {
|
||||||
|
targetConfig = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholderPaths.length === 0) {
|
||||||
|
return JSON.stringify(targetConfig, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mutatedConfig = targetConfig;
|
||||||
|
|
||||||
|
for (const path of placeholderPaths) {
|
||||||
|
const nextValue = getValueAtPath(replacedConfig, path);
|
||||||
|
if (path.length === 0) {
|
||||||
|
mutatedConfig = nextValue;
|
||||||
|
} else {
|
||||||
|
setValueAtPath(mutatedConfig, path, nextValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(mutatedConfig, null, 2);
|
||||||
|
} catch {
|
||||||
|
return JSON.stringify(replacedConfig, null, 2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet";
|
const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet";
|
||||||
const CODEX_COMMON_CONFIG_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
const CODEX_COMMON_CONFIG_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
||||||
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
||||||
@@ -71,13 +208,26 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [claudeModel, setClaudeModel] = useState("");
|
const [claudeModel, setClaudeModel] = useState("");
|
||||||
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
|
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
|
||||||
const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态
|
const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态
|
||||||
|
// 模板变量状态
|
||||||
|
const [templateValues, setTemplateValues] = useState<
|
||||||
|
Record<string, TemplateValueConfig>
|
||||||
|
>({});
|
||||||
|
|
||||||
// Codex 特有的状态
|
// Codex 特有的状态
|
||||||
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);
|
||||||
|
// 新建供应商:收集端点测速弹窗中的“自定义端点”,提交时一次性落盘到 meta.custom_endpoints
|
||||||
|
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
// 端点测速弹窗状态
|
||||||
|
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
||||||
|
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
|
||||||
|
useState(false);
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||||
showPresets && isCodex ? -1 : null
|
showPresets && isCodex ? -1 : null
|
||||||
@@ -88,8 +238,12 @@ 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) => {
|
||||||
@@ -103,6 +257,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") {
|
||||||
@@ -157,6 +315,9 @@ 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>(
|
||||||
showPresets ? -1 : null
|
showPresets ? -1 : null
|
||||||
@@ -300,6 +461,43 @@ 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;
|
||||||
@@ -377,6 +575,22 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setError(currentSettingsError);
|
setError(currentSettingsError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedTemplatePreset && templateValueEntries.length > 0) {
|
||||||
|
for (const [key, config] of templateValueEntries) {
|
||||||
|
const entry = templateValues[key];
|
||||||
|
const resolvedValue = (
|
||||||
|
entry?.editorValue ??
|
||||||
|
entry?.defaultValue ??
|
||||||
|
config.defaultValue ??
|
||||||
|
""
|
||||||
|
).trim();
|
||||||
|
if (!resolvedValue) {
|
||||||
|
setError(`请填写 ${config.label}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Claude: 原有逻辑
|
// Claude: 原有逻辑
|
||||||
if (!formData.settingsConfig.trim()) {
|
if (!formData.settingsConfig.trim()) {
|
||||||
setError("请填写配置内容");
|
setError("请填写配置内容");
|
||||||
@@ -391,13 +605,31 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit({
|
// 构造基础提交数据
|
||||||
|
const basePayload: Omit<Provider, "id"> = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
websiteUrl: formData.websiteUrl,
|
websiteUrl: formData.websiteUrl,
|
||||||
settingsConfig,
|
settingsConfig,
|
||||||
// 仅在用户选择了预设或手动选择“自定义”时持久化分类
|
// 仅在用户选择了预设或手动选择“自定义”时持久化分类
|
||||||
...(category ? { category } : {}),
|
...(category ? { category } : {}),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// 若为“新建供应商”,且已在弹窗中添加了自定义端点,则随提交一并落盘
|
||||||
|
if (!initialData && draftCustomEndpoints.length > 0) {
|
||||||
|
const now = Date.now();
|
||||||
|
const customMap: Record<string, CustomEndpoint> = {};
|
||||||
|
for (const raw of draftCustomEndpoints) {
|
||||||
|
const url = raw.trim().replace(/\/+$/, "");
|
||||||
|
if (!url) continue;
|
||||||
|
if (!customMap[url]) {
|
||||||
|
customMap[url] = { url, addedAt: now };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSubmit({ ...basePayload, meta: { custom_endpoints: customMap } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(basePayload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
@@ -529,7 +761,30 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyPreset = (preset: (typeof providerPresets)[0], index: number) => {
|
const applyPreset = (preset: (typeof providerPresets)[0], index: number) => {
|
||||||
const configString = JSON.stringify(preset.settingsConfig, null, 2);
|
let appliedSettingsConfig = preset.settingsConfig;
|
||||||
|
let initialTemplateValues: TemplateValueMap = {};
|
||||||
|
|
||||||
|
if (preset.templateValues) {
|
||||||
|
initialTemplateValues = Object.fromEntries(
|
||||||
|
Object.entries(preset.templateValues).map(([key, config]) => [
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
editorValue: config.editorValue
|
||||||
|
? config.editorValue
|
||||||
|
: (config.defaultValue ?? ""),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
appliedSettingsConfig = applyTemplateValues(
|
||||||
|
preset.settingsConfig,
|
||||||
|
initialTemplateValues
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTemplateValues(initialTemplateValues);
|
||||||
|
|
||||||
|
const configString = JSON.stringify(appliedSettingsConfig, null, 2);
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
@@ -546,7 +801,6 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
// 清空 API Key 输入框,让用户重新输入
|
// 清空 API Key 输入框,让用户重新输入
|
||||||
setApiKey("");
|
setApiKey("");
|
||||||
setBaseUrl(""); // 清空基础 URL
|
|
||||||
|
|
||||||
// 同步通用配置状态
|
// 同步通用配置状态
|
||||||
const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet);
|
const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet);
|
||||||
@@ -554,11 +808,16 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setCommonConfigError("");
|
setCommonConfigError("");
|
||||||
|
|
||||||
// 如果预设包含模型配置,初始化模型输入框
|
// 如果预设包含模型配置,初始化模型输入框
|
||||||
if (preset.settingsConfig && typeof preset.settingsConfig === "object") {
|
if (appliedSettingsConfig && typeof appliedSettingsConfig === "object") {
|
||||||
const config = preset.settingsConfig as { env?: Record<string, any> };
|
const config = appliedSettingsConfig as { env?: Record<string, any> };
|
||||||
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")) {
|
||||||
@@ -570,6 +829,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
setClaudeModel("");
|
setClaudeModel("");
|
||||||
setClaudeSmallFastModel("");
|
setClaudeSmallFastModel("");
|
||||||
|
setBaseUrl("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -577,6 +837,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 处理点击自定义按钮
|
// 处理点击自定义按钮
|
||||||
const handleCustomClick = () => {
|
const handleCustomClick = () => {
|
||||||
setSelectedPreset(-1);
|
setSelectedPreset(-1);
|
||||||
|
setTemplateValues({});
|
||||||
|
|
||||||
// 设置自定义模板
|
// 设置自定义模板
|
||||||
const customTemplate = {
|
const customTemplate = {
|
||||||
@@ -615,6 +876,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,
|
||||||
@@ -652,6 +917,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");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -675,21 +941,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);
|
||||||
@@ -795,6 +1082,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 输入框
|
||||||
@@ -803,6 +1096,25 @@ 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 =
|
||||||
|
!isCodex &&
|
||||||
|
selectedPreset !== null &&
|
||||||
|
selectedPreset >= 0 &&
|
||||||
|
selectedPreset < providerPresets.length
|
||||||
|
? providerPresets[selectedPreset]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const templateValueEntries: Array<[string, TemplateValueConfig]> =
|
||||||
|
selectedTemplatePreset?.templateValues
|
||||||
|
? (Object.entries(selectedTemplatePreset.templateValues) as Array<
|
||||||
|
[string, TemplateValueConfig]
|
||||||
|
>)
|
||||||
|
: [];
|
||||||
|
|
||||||
// 判断当前选中的预设是否是官方
|
// 判断当前选中的预设是否是官方
|
||||||
const isOfficialPreset =
|
const isOfficialPreset =
|
||||||
(selectedPreset !== null &&
|
(selectedPreset !== null &&
|
||||||
@@ -828,8 +1140,88 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 综合判断是否应该显示 Kimi 模型选择器
|
// 综合判断是否应该显示 Kimi 模型选择器
|
||||||
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
||||||
|
|
||||||
// 判断是否显示基础 URL 输入框(仅自定义模式显示)
|
const claudeSpeedTestEndpoints = useMemo<EndpointCandidate[]>(() => {
|
||||||
const showBaseUrlInput = selectedPreset === -1 && !isCodex;
|
if (isCodex) return [];
|
||||||
|
const map = new Map<string, EndpointCandidate>();
|
||||||
|
const add = (url?: string) => {
|
||||||
|
if (!url) return;
|
||||||
|
const sanitized = url.trim().replace(/\/+$/, "");
|
||||||
|
if (!sanitized || map.has(sanitized)) return;
|
||||||
|
map.set(sanitized, { url: sanitized });
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
// 合并预设内置的请求地址候选
|
||||||
|
if (Array.isArray((preset as any).endpointCandidates)) {
|
||||||
|
((preset as any).endpointCandidates as string[]).forEach((u) => add(u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (!url) return;
|
||||||
|
const sanitized = url.trim().replace(/\/+$/, "");
|
||||||
|
if (!sanitized || map.has(sanitized)) return;
|
||||||
|
map.set(sanitized, { url: sanitized });
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedCodexPreset !== null &&
|
||||||
|
selectedCodexPreset >= 0 &&
|
||||||
|
selectedCodexPreset < codexProviderPresets.length
|
||||||
|
) {
|
||||||
|
const preset = codexProviderPresets[selectedCodexPreset];
|
||||||
|
const presetBase = extractCodexBaseUrl(preset?.config || "");
|
||||||
|
if (presetBase) {
|
||||||
|
add(presetBase);
|
||||||
|
}
|
||||||
|
// 合并预设内置的请求地址候选
|
||||||
|
if (Array.isArray((preset as any)?.endpointCandidates)) {
|
||||||
|
((preset as any).endpointCandidates as string[]).forEach((u) => add(u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
}, [isCodex, codexBaseUrl, initialData, selectedCodexPreset]);
|
||||||
|
|
||||||
// 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示)
|
// 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示)
|
||||||
const shouldShowApiKeyLink =
|
const shouldShowApiKeyLink =
|
||||||
@@ -977,13 +1369,26 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
|
// 若有子弹窗(端点测速/模板向导)处于打开状态,则交由子弹窗自身处理,避免级联关闭
|
||||||
|
if (
|
||||||
|
isEndpointModalOpen ||
|
||||||
|
isCodexEndpointModalOpen ||
|
||||||
|
isCodexTemplateModalOpen
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", onKeyDown);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
}, [onClose]);
|
}, [
|
||||||
|
onClose,
|
||||||
|
isEndpointModalOpen,
|
||||||
|
isCodexEndpointModalOpen,
|
||||||
|
isCodexTemplateModalOpen,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -1133,15 +1538,95 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 基础 URL 输入框 - 仅在自定义模式下显示 */}
|
{!isCodex &&
|
||||||
{!isCodex && showBaseUrlInput && (
|
selectedTemplatePreset &&
|
||||||
|
templateValueEntries.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
参数配置 - {selectedTemplatePreset.name.trim()} *
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{templateValueEntries.map(([key, config]) => (
|
||||||
|
<div key={key} className="space-y-2">
|
||||||
|
<label className="sr-only" htmlFor={`template-${key}`}>
|
||||||
|
{config.label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={`template-${key}`}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder={`${config.label} *`}
|
||||||
|
value={
|
||||||
|
templateValues[key]?.editorValue ??
|
||||||
|
config.editorValue ??
|
||||||
|
config.defaultValue ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setTemplateValues((prev) => {
|
||||||
|
const prevEntry = prev[key];
|
||||||
|
const nextEntry: TemplateValueConfig = {
|
||||||
|
...config,
|
||||||
|
...(prevEntry ?? {}),
|
||||||
|
editorValue: newValue,
|
||||||
|
};
|
||||||
|
const nextValues: TemplateValueMap = {
|
||||||
|
...prev,
|
||||||
|
[key]: nextEntry,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedTemplatePreset) {
|
||||||
|
try {
|
||||||
|
const configString =
|
||||||
|
applyTemplateValuesToConfigString(
|
||||||
|
selectedTemplatePreset.settingsConfig,
|
||||||
|
formData.settingsConfig,
|
||||||
|
nextValues
|
||||||
|
);
|
||||||
|
setFormData((prevForm) => ({
|
||||||
|
...prevForm,
|
||||||
|
settingsConfig: configString,
|
||||||
|
}));
|
||||||
|
setSettingsConfigError(
|
||||||
|
validateSettingsConfig(configString)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("更新模板值失败:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextValues;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
aria-label={config.label}
|
||||||
|
autoComplete="off"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isCodex && shouldShowSpeedTest && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label
|
<div className="flex items-center justify-between">
|
||||||
htmlFor="baseUrl"
|
<label
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
htmlFor="baseUrl"
|
||||||
>
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
请求地址
|
>
|
||||||
</label>
|
请求地址
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEndpointModalOpen(true)}
|
||||||
|
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<Zap className="h-3.5 w-3.5" />
|
||||||
|
管理与测速
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
id="baseUrl"
|
id="baseUrl"
|
||||||
@@ -1159,6 +1644,20 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 端点测速弹窗 - Claude */}
|
||||||
|
{!isCodex && shouldShowSpeedTest && isEndpointModalOpen && (
|
||||||
|
<EndpointSpeedTest
|
||||||
|
appType={appType}
|
||||||
|
providerId={initialData?.id}
|
||||||
|
value={baseUrl}
|
||||||
|
onChange={handleBaseUrlChange}
|
||||||
|
initialEndpoints={claudeSpeedTestEndpoints}
|
||||||
|
visible={isEndpointModalOpen}
|
||||||
|
onClose={() => setIsEndpointModalOpen(false)}
|
||||||
|
onCustomEndpointsChange={setDraftCustomEndpoints}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isCodex && shouldShowKimiSelector && (
|
{!isCodex && shouldShowKimiSelector && (
|
||||||
<KimiModelSelector
|
<KimiModelSelector
|
||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
@@ -1203,6 +1702,50 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isCodex && shouldShowSpeedTest && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
htmlFor="codexBaseUrl"
|
||||||
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
请求地址
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsCodexEndpointModalOpen(true)}
|
||||||
|
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<Zap className="h-3.5 w-3.5" />
|
||||||
|
管理与测速
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="codexBaseUrl"
|
||||||
|
value={codexBaseUrl}
|
||||||
|
onChange={(e) => handleCodexBaseUrlChange(e.target.value)}
|
||||||
|
placeholder="https://your-api-endpoint.com/v1"
|
||||||
|
autoComplete="off"
|
||||||
|
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 端点测速弹窗 - Codex */}
|
||||||
|
{isCodex && shouldShowSpeedTest && isCodexEndpointModalOpen && (
|
||||||
|
<EndpointSpeedTest
|
||||||
|
appType={appType}
|
||||||
|
providerId={initialData?.id}
|
||||||
|
value={codexBaseUrl}
|
||||||
|
onChange={handleCodexBaseUrlChange}
|
||||||
|
initialEndpoints={codexSpeedTestEndpoints}
|
||||||
|
visible={isCodexEndpointModalOpen}
|
||||||
|
onClose={() => setIsCodexEndpointModalOpen(false)}
|
||||||
|
onCustomEndpointsChange={setDraftCustomEndpoints}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Claude 或 Codex 的配置部分 */}
|
{/* Claude 或 Codex 的配置部分 */}
|
||||||
{isCodex ? (
|
{isCodex ? (
|
||||||
<CodexConfigEditor
|
<CodexConfigEditor
|
||||||
|
|||||||
602
src/components/ProviderForm/EndpointSpeedTest.tsx
Normal file
602
src/components/ProviderForm/EndpointSpeedTest.tsx
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Zap, Loader2, Plus, X, AlertCircle } from "lucide-react";
|
||||||
|
import { isLinux } from "../../lib/platform";
|
||||||
|
|
||||||
|
import type { AppType } from "../../lib/tauri-api";
|
||||||
|
|
||||||
|
export interface EndpointCandidate {
|
||||||
|
id?: string;
|
||||||
|
url: string;
|
||||||
|
isCustom?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EndpointSpeedTestProps {
|
||||||
|
appType: AppType;
|
||||||
|
providerId?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (url: string) => void;
|
||||||
|
initialEndpoints: EndpointCandidate[];
|
||||||
|
visible?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
// 当自定义端点列表变化时回传(仅包含 isCustom 的条目)
|
||||||
|
onCustomEndpointsChange?: (urls: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EndpointEntry extends EndpointCandidate {
|
||||||
|
id: string;
|
||||||
|
latency: number | null;
|
||||||
|
status?: number;
|
||||||
|
error?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomId = () => `ep_${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
|
||||||
|
const normalizeEndpointUrl = (url: string): string =>
|
||||||
|
url.trim().replace(/\/+$/, "");
|
||||||
|
|
||||||
|
const buildInitialEntries = (
|
||||||
|
candidates: EndpointCandidate[],
|
||||||
|
selected: string,
|
||||||
|
): EndpointEntry[] => {
|
||||||
|
const map = new Map<string, EndpointEntry>();
|
||||||
|
const addCandidate = (candidate: EndpointCandidate) => {
|
||||||
|
const sanitized = candidate.url ? normalizeEndpointUrl(candidate.url) : "";
|
||||||
|
if (!sanitized) return;
|
||||||
|
if (map.has(sanitized)) return;
|
||||||
|
|
||||||
|
map.set(sanitized, {
|
||||||
|
id: candidate.id ?? randomId(),
|
||||||
|
url: sanitized,
|
||||||
|
isCustom: candidate.isCustom ?? false,
|
||||||
|
latency: null,
|
||||||
|
status: undefined,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
candidates.forEach(addCandidate);
|
||||||
|
|
||||||
|
const selectedUrl = normalizeEndpointUrl(selected);
|
||||||
|
if (selectedUrl && !map.has(selectedUrl)) {
|
||||||
|
addCandidate({ url: selectedUrl, isCustom: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||||
|
appType,
|
||||||
|
providerId,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
initialEndpoints,
|
||||||
|
visible = true,
|
||||||
|
onClose,
|
||||||
|
onCustomEndpointsChange,
|
||||||
|
}) => {
|
||||||
|
const [entries, setEntries] = useState<EndpointEntry[]>(() =>
|
||||||
|
buildInitialEntries(initialEndpoints, value),
|
||||||
|
);
|
||||||
|
const [customUrl, setCustomUrl] = useState("");
|
||||||
|
const [addError, setAddError] = useState<string | null>(null);
|
||||||
|
const [autoSelect, setAutoSelect] = useState(true);
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [lastError, setLastError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const normalizedSelected = normalizeEndpointUrl(value);
|
||||||
|
|
||||||
|
const hasEndpoints = entries.length > 0;
|
||||||
|
|
||||||
|
// 加载保存的自定义端点(按正在编辑的供应商)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCustomEndpoints = async () => {
|
||||||
|
try {
|
||||||
|
if (!providerId) return;
|
||||||
|
const customEndpoints = await window.api.getCustomEndpoints(
|
||||||
|
appType,
|
||||||
|
providerId,
|
||||||
|
);
|
||||||
|
const candidates: EndpointCandidate[] = customEndpoints.map((ep) => ({
|
||||||
|
url: ep.url,
|
||||||
|
isCustom: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setEntries((prev) => {
|
||||||
|
const map = new Map<string, EndpointEntry>();
|
||||||
|
|
||||||
|
// 先添加现有端点
|
||||||
|
prev.forEach((entry) => {
|
||||||
|
map.set(entry.url, entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 合并自定义端点
|
||||||
|
candidates.forEach((candidate) => {
|
||||||
|
const sanitized = normalizeEndpointUrl(candidate.url);
|
||||||
|
if (sanitized && !map.has(sanitized)) {
|
||||||
|
map.set(sanitized, {
|
||||||
|
id: randomId(),
|
||||||
|
url: sanitized,
|
||||||
|
isCustom: true,
|
||||||
|
latency: null,
|
||||||
|
status: undefined,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载自定义端点失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
loadCustomEndpoints();
|
||||||
|
}
|
||||||
|
}, [appType, visible, providerId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEntries((prev) => {
|
||||||
|
const map = new Map<string, EndpointEntry>();
|
||||||
|
prev.forEach((entry) => {
|
||||||
|
map.set(entry.url, entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
const mergeCandidate = (candidate: EndpointCandidate) => {
|
||||||
|
const sanitized = candidate.url
|
||||||
|
? normalizeEndpointUrl(candidate.url)
|
||||||
|
: "";
|
||||||
|
if (!sanitized) return;
|
||||||
|
const existing = map.get(sanitized);
|
||||||
|
if (existing) return;
|
||||||
|
|
||||||
|
map.set(sanitized, {
|
||||||
|
id: candidate.id ?? randomId(),
|
||||||
|
url: sanitized,
|
||||||
|
isCustom: candidate.isCustom ?? false,
|
||||||
|
latency: null,
|
||||||
|
status: undefined,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
changed = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
initialEndpoints.forEach(mergeCandidate);
|
||||||
|
|
||||||
|
if (normalizedSelected && !map.has(normalizedSelected)) {
|
||||||
|
mergeCandidate({ url: normalizedSelected, isCustom: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
});
|
||||||
|
}, [initialEndpoints, normalizedSelected]);
|
||||||
|
|
||||||
|
// 将自定义端点变化透传给父组件(仅限 isCustom)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onCustomEndpointsChange) return;
|
||||||
|
try {
|
||||||
|
const customUrls = Array.from(
|
||||||
|
new Set(
|
||||||
|
entries
|
||||||
|
.filter((e) => e.isCustom)
|
||||||
|
.map((e) => (e.url ? normalizeEndpointUrl(e.url) : ""))
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
onCustomEndpointsChange(customUrls);
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
// 仅在 entries 变化时同步
|
||||||
|
}, [entries, onCustomEndpointsChange]);
|
||||||
|
|
||||||
|
const sortedEntries = useMemo(() => {
|
||||||
|
return entries.slice().sort((a, b) => {
|
||||||
|
const aLatency = a.latency ?? Number.POSITIVE_INFINITY;
|
||||||
|
const bLatency = b.latency ?? Number.POSITIVE_INFINITY;
|
||||||
|
if (aLatency === bLatency) {
|
||||||
|
return a.url.localeCompare(b.url);
|
||||||
|
}
|
||||||
|
return aLatency - bLatency;
|
||||||
|
});
|
||||||
|
}, [entries]);
|
||||||
|
|
||||||
|
const handleAddEndpoint = useCallback(
|
||||||
|
async () => {
|
||||||
|
const candidate = customUrl.trim();
|
||||||
|
let errorMsg: string | null = null;
|
||||||
|
|
||||||
|
if (!candidate) {
|
||||||
|
errorMsg = "请输入有效的 URL";
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: URL | null = null;
|
||||||
|
if (!errorMsg) {
|
||||||
|
try {
|
||||||
|
parsed = new URL(candidate);
|
||||||
|
} catch {
|
||||||
|
errorMsg = "URL 格式不正确";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errorMsg && parsed && !parsed.protocol.startsWith("http")) {
|
||||||
|
errorMsg = "仅支持 HTTP/HTTPS";
|
||||||
|
}
|
||||||
|
|
||||||
|
let sanitized = "";
|
||||||
|
if (!errorMsg && parsed) {
|
||||||
|
sanitized = normalizeEndpointUrl(parsed.toString());
|
||||||
|
// 使用当前 entries 做去重校验,避免依赖可能过期的 addError
|
||||||
|
const isDuplicate = entries.some((entry) => entry.url === sanitized);
|
||||||
|
if (isDuplicate) {
|
||||||
|
errorMsg = "该地址已存在";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMsg) {
|
||||||
|
setAddError(errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAddError(null);
|
||||||
|
|
||||||
|
// 保存到后端
|
||||||
|
try {
|
||||||
|
if (providerId) {
|
||||||
|
await window.api.addCustomEndpoint(appType, providerId, sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
|
setEntries((prev) => {
|
||||||
|
if (prev.some((e) => e.url === sanitized)) return prev;
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: randomId(),
|
||||||
|
url: sanitized,
|
||||||
|
isCustom: true,
|
||||||
|
latency: null,
|
||||||
|
status: undefined,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!normalizedSelected) {
|
||||||
|
onChange(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCustomUrl("");
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
setAddError(message || "保存失败,请重试");
|
||||||
|
console.error("添加自定义端点失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[customUrl, entries, normalizedSelected, onChange, appType, providerId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveEndpoint = useCallback(
|
||||||
|
async (entry: EndpointEntry) => {
|
||||||
|
// 如果是自定义端点,尝试从后端删除(无 providerId 则仅本地删除)
|
||||||
|
if (entry.isCustom && providerId) {
|
||||||
|
try {
|
||||||
|
await window.api.removeCustomEndpoint(appType, providerId, entry.url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除自定义端点失败:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
|
setEntries((prev) => {
|
||||||
|
const next = prev.filter((item) => item.id !== entry.id);
|
||||||
|
if (entry.url === normalizedSelected) {
|
||||||
|
const fallback = next[0];
|
||||||
|
onChange(fallback ? fallback.url : "");
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[normalizedSelected, onChange, appType, providerId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const runSpeedTest = useCallback(async () => {
|
||||||
|
const urls = entries.map((entry) => entry.url);
|
||||||
|
if (urls.length === 0) {
|
||||||
|
setLastError("请先添加端点");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window === "undefined" || !window.api?.testApiEndpoints) {
|
||||||
|
setLastError("测速功能不可用");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTesting(true);
|
||||||
|
setLastError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await window.api.testApiEndpoints(urls, {
|
||||||
|
timeoutSecs: appType === "codex" ? 12 : 8,
|
||||||
|
});
|
||||||
|
const resultMap = new Map(
|
||||||
|
results.map((item) => [normalizeEndpointUrl(item.url), item]),
|
||||||
|
);
|
||||||
|
|
||||||
|
setEntries((prev) =>
|
||||||
|
prev.map((entry) => {
|
||||||
|
const match = resultMap.get(entry.url);
|
||||||
|
if (!match) {
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
latency: null,
|
||||||
|
status: undefined,
|
||||||
|
error: "未返回结果",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
latency:
|
||||||
|
typeof match.latency === "number" ? Math.round(match.latency) : null,
|
||||||
|
status: match.status,
|
||||||
|
error: match.error ?? null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (autoSelect) {
|
||||||
|
const successful = results
|
||||||
|
.filter((item) => typeof item.latency === "number" && item.latency !== null)
|
||||||
|
.sort((a, b) => (a.latency! || 0) - (b.latency! || 0));
|
||||||
|
const best = successful[0];
|
||||||
|
if (best && best.url && best.url !== normalizedSelected) {
|
||||||
|
onChange(best.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : `测速失败: ${String(error)}`;
|
||||||
|
setLastError(message);
|
||||||
|
} finally {
|
||||||
|
setIsTesting(false);
|
||||||
|
}
|
||||||
|
}, [entries, autoSelect, appType, normalizedSelected, onChange]);
|
||||||
|
|
||||||
|
const handleSelect = useCallback(
|
||||||
|
async (url: string) => {
|
||||||
|
if (!url || url === normalizedSelected) return;
|
||||||
|
|
||||||
|
// 更新最后使用时间(对自定义端点)
|
||||||
|
const entry = entries.find((e) => e.url === url);
|
||||||
|
if (entry?.isCustom && providerId) {
|
||||||
|
await window.api.updateEndpointLastUsed(appType, providerId, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(url);
|
||||||
|
},
|
||||||
|
[normalizedSelected, onChange, appType, entries, providerId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 支持按下 ESC 关闭弹窗
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||||
|
isLinux() ? "" : " backdrop-blur-sm"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg w-full max-w-2xl mx-4 max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<h3 className="text-base font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
请求地址管理
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-auto px-6 py-4 space-y-4">
|
||||||
|
{/* 测速控制栏 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{entries.length} 个端点
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<label className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoSelect}
|
||||||
|
onChange={(event) => setAutoSelect(event.target.checked)}
|
||||||
|
className="h-3.5 w-3.5 rounded border-gray-300 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
自动选择
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={runSpeedTest}
|
||||||
|
disabled={isTesting || !hasEndpoints}
|
||||||
|
className="flex h-7 items-center gap-1.5 rounded-md bg-blue-500 px-2.5 text-xs font-medium text-white transition hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
{isTesting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
测速中
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Zap className="h-3.5 w-3.5" />
|
||||||
|
测速
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 添加输入 */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={customUrl}
|
||||||
|
placeholder="https://api.example.com"
|
||||||
|
onChange={(event) => setCustomUrl(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
handleAddEndpoint();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex-1 rounded-md border border-gray-200 bg-white px-3 py-1.5 text-sm text-gray-900 placeholder-gray-400 transition focus:border-gray-400 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-gray-600"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddEndpoint}
|
||||||
|
className="flex h-8 w-8 items-center justify-center rounded-md border border-gray-200 transition hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{addError && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-red-600 dark:text-red-400">
|
||||||
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
{addError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 端点列表 */}
|
||||||
|
{hasEndpoints ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{sortedEntries.map((entry) => {
|
||||||
|
const isSelected = normalizedSelected === entry.url;
|
||||||
|
const latency = entry.latency;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={entry.id}
|
||||||
|
onClick={() => handleSelect(entry.url)}
|
||||||
|
className={`group flex cursor-pointer items-center justify-between px-3 py-2.5 rounded-lg border transition ${
|
||||||
|
isSelected
|
||||||
|
? "border-blue-500 bg-blue-50 dark:border-blue-500 dark:bg-blue-900/20"
|
||||||
|
: "border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:hover:border-gray-600 dark:hover:bg-gray-850"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
|
{/* 选择指示器 */}
|
||||||
|
<div
|
||||||
|
className={`h-1.5 w-1.5 flex-shrink-0 rounded-full transition ${
|
||||||
|
isSelected
|
||||||
|
? "bg-blue-500 dark:bg-blue-400"
|
||||||
|
: "bg-gray-300 dark:bg-gray-700"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-sm text-gray-900 dark:text-gray-100">
|
||||||
|
{entry.url}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧信息 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{latency !== null ? (
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="font-mono text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{latency}ms
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : isTesting ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-gray-400" />
|
||||||
|
) : entry.error ? (
|
||||||
|
<div className="text-xs text-gray-400">失败</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-gray-400">—</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemoveEndpoint(entry);
|
||||||
|
}}
|
||||||
|
className="opacity-0 transition hover:text-red-600 group-hover:opacity-100 dark:hover:text-red-400"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md border border-dashed border-gray-200 bg-gray-50 py-8 text-center text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400">
|
||||||
|
暂无端点
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{lastError && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-red-600 dark:text-red-400">
|
||||||
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
{lastError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EndpointSpeedTest;
|
||||||
@@ -4,13 +4,6 @@ import { Provider } from "../types";
|
|||||||
import { Play, Edit3, Trash2, CheckCircle2, Users, Check } from "lucide-react";
|
import { Play, Edit3, Trash2, CheckCircle2, Users, Check } from "lucide-react";
|
||||||
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
||||||
import { AppType } from "../lib/tauri-api";
|
import { AppType } from "../lib/tauri-api";
|
||||||
import {
|
|
||||||
applyProviderToVSCode,
|
|
||||||
detectApplied,
|
|
||||||
normalizeBaseUrl,
|
|
||||||
} from "../utils/vscodeSettings";
|
|
||||||
import { getCodexBaseUrl } from "../utils/providerConfigUtils";
|
|
||||||
import { useVSCodeAutoSync } from "../hooks/useVSCodeAutoSync";
|
|
||||||
// 不再在列表中显示分类徽章,避免造成困惑
|
// 不再在列表中显示分类徽章,避免造成困惑
|
||||||
|
|
||||||
interface ProviderListProps {
|
interface ProviderListProps {
|
||||||
@@ -65,46 +58,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析 Codex 配置中的 base_url(已提取到公共工具)
|
|
||||||
|
|
||||||
// VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否"已应用"变化
|
|
||||||
const [vscodeAppliedFor, setVscodeAppliedFor] = useState<string | null>(null);
|
|
||||||
const { enableAutoSync, disableAutoSync } = useVSCodeAutoSync();
|
|
||||||
const [claudeApplied, setClaudeApplied] = useState<boolean>(false);
|
const [claudeApplied, setClaudeApplied] = useState<boolean>(false);
|
||||||
|
|
||||||
// 当当前供应商或 appType 变化时,尝试读取 VS Code settings 并检测状态
|
|
||||||
useEffect(() => {
|
|
||||||
const check = async () => {
|
|
||||||
if (appType !== "codex" || !currentProviderId) {
|
|
||||||
setVscodeAppliedFor(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const status = await window.api.getVSCodeSettingsStatus();
|
|
||||||
if (!status.exists) {
|
|
||||||
setVscodeAppliedFor(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const content = await window.api.readVSCodeSettings();
|
|
||||||
const detected = detectApplied(content);
|
|
||||||
// 认为“已应用”的条件(非官方供应商):VS Code 中的 apiBase 与当前供应商的 base_url 完全一致
|
|
||||||
const current = providers[currentProviderId];
|
|
||||||
let applied = false;
|
|
||||||
if (current && current.category !== "official") {
|
|
||||||
const base = getCodexBaseUrl(current);
|
|
||||||
if (detected.apiBase && base) {
|
|
||||||
applied =
|
|
||||||
normalizeBaseUrl(detected.apiBase) === normalizeBaseUrl(base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setVscodeAppliedFor(applied ? currentProviderId : null);
|
|
||||||
} catch {
|
|
||||||
setVscodeAppliedFor(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
check();
|
|
||||||
}, [appType, currentProviderId, providers]);
|
|
||||||
|
|
||||||
// 检查 Claude 插件配置是否已应用
|
// 检查 Claude 插件配置是否已应用
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkClaude = async () => {
|
const checkClaude = async () => {
|
||||||
@@ -123,83 +78,6 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
checkClaude();
|
checkClaude();
|
||||||
}, [appType, currentProviderId, providers]);
|
}, [appType, currentProviderId, providers]);
|
||||||
|
|
||||||
const handleApplyToVSCode = async (provider: Provider) => {
|
|
||||||
try {
|
|
||||||
const status = await window.api.getVSCodeSettingsStatus();
|
|
||||||
if (!status.exists) {
|
|
||||||
onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = await window.api.readVSCodeSettings();
|
|
||||||
|
|
||||||
const isOfficial = provider.category === "official";
|
|
||||||
// 非官方且缺少 base_url 时直接报错并返回,避免“空写入”假成功
|
|
||||||
if (!isOfficial) {
|
|
||||||
const parsed = getCodexBaseUrl(provider);
|
|
||||||
if (!parsed) {
|
|
||||||
onNotify?.(t("notifications.missingBaseUrl"), "error", 4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl = isOfficial ? undefined : getCodexBaseUrl(provider);
|
|
||||||
const next = applyProviderToVSCode(raw, { baseUrl, isOfficial });
|
|
||||||
|
|
||||||
if (next === raw) {
|
|
||||||
// 幂等:没有变化也提示成功
|
|
||||||
onNotify?.(t("notifications.appliedToVSCode"), "success", 3000);
|
|
||||||
setVscodeAppliedFor(provider.id);
|
|
||||||
// 用户手动应用时,启用自动同步
|
|
||||||
enableAutoSync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await window.api.writeVSCodeSettings(next);
|
|
||||||
onNotify?.(t("notifications.appliedToVSCode"), "success", 3000);
|
|
||||||
setVscodeAppliedFor(provider.id);
|
|
||||||
// 用户手动应用时,启用自动同步
|
|
||||||
enableAutoSync();
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
const msg =
|
|
||||||
e && e.message ? e.message : t("notifications.syncVSCodeFailed");
|
|
||||||
onNotify?.(msg, "error", 5000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveFromVSCode = async () => {
|
|
||||||
try {
|
|
||||||
const status = await window.api.getVSCodeSettingsStatus();
|
|
||||||
if (!status.exists) {
|
|
||||||
onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const raw = await window.api.readVSCodeSettings();
|
|
||||||
const next = applyProviderToVSCode(raw, {
|
|
||||||
baseUrl: undefined,
|
|
||||||
isOfficial: true,
|
|
||||||
});
|
|
||||||
if (next === raw) {
|
|
||||||
onNotify?.(t("notifications.removedFromVSCode"), "success", 3000);
|
|
||||||
setVscodeAppliedFor(null);
|
|
||||||
// 用户手动移除时,禁用自动同步
|
|
||||||
disableAutoSync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await window.api.writeVSCodeSettings(next);
|
|
||||||
onNotify?.(t("notifications.removedFromVSCode"), "success", 3000);
|
|
||||||
setVscodeAppliedFor(null);
|
|
||||||
// 用户手动移除时,禁用自动同步
|
|
||||||
disableAutoSync();
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
const msg =
|
|
||||||
e && e.message ? e.message : t("notifications.syncVSCodeFailed");
|
|
||||||
onNotify?.(msg, "error", 5000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApplyToClaudePlugin = async () => {
|
const handleApplyToClaudePlugin = async () => {
|
||||||
try {
|
try {
|
||||||
await window.api.applyClaudePluginConfig({ official: false });
|
await window.api.applyClaudePluginConfig({ official: false });
|
||||||
@@ -320,38 +198,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<div className="flex items-center gap-2 ml-4">
|
||||||
{/* 同步按钮占位容器 - 只在对应模式下渲染,避免布局跳动 */}
|
|
||||||
{appType === "codex" ? (
|
|
||||||
<div className="w-[130px]">
|
|
||||||
{provider.category !== "official" && isCurrent && (
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
vscodeAppliedFor === provider.id
|
|
||||||
? handleRemoveFromVSCode()
|
|
||||||
: handleApplyToVSCode(provider)
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-full whitespace-nowrap justify-center",
|
|
||||||
vscodeAppliedFor === provider.id
|
|
||||||
? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20"
|
|
||||||
: "border border-gray-300 text-gray-700 hover:border-blue-300 hover:text-blue-600 hover:bg-blue-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-700 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
|
|
||||||
)}
|
|
||||||
title={
|
|
||||||
vscodeAppliedFor === provider.id
|
|
||||||
? t("provider.removeFromVSCode")
|
|
||||||
: t("provider.applyToVSCode")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{vscodeAppliedFor === provider.id
|
|
||||||
? t("provider.removeFromVSCode")
|
|
||||||
: t("provider.applyToVSCode")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{appType === "claude" ? (
|
{appType === "claude" ? (
|
||||||
<div className="w-[130px]">
|
<div className="flex-shrink-0">
|
||||||
{provider.category !== "official" && isCurrent && (
|
{provider.category !== "official" && isCurrent && (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Save,
|
Save,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
import { ImportProgressModal } from "./ImportProgressModal";
|
||||||
import { homeDir, join } from "@tauri-apps/api/path";
|
import { homeDir, join } from "@tauri-apps/api/path";
|
||||||
import "../lib/tauri-api";
|
import "../lib/tauri-api";
|
||||||
import { relaunchApp } from "../lib/updater";
|
import { relaunchApp } from "../lib/updater";
|
||||||
@@ -22,9 +23,10 @@ import { isLinux } from "../lib/platform";
|
|||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onImportSuccess?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsModal({ onClose }: SettingsModalProps) {
|
export default function SettingsModal({ onClose, onImportSuccess }: SettingsModalProps) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
const normalizeLanguage = (lang?: string | null): "zh" | "en" =>
|
const normalizeLanguage = (lang?: string | null): "zh" | "en" =>
|
||||||
@@ -63,6 +65,13 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } =
|
const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } =
|
||||||
useUpdate();
|
useUpdate();
|
||||||
|
|
||||||
|
// 导入/导出相关状态
|
||||||
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
const [importStatus, setImportStatus] = useState<'idle' | 'importing' | 'success' | 'error'>('idle');
|
||||||
|
const [importError, setImportError] = useState<string>("");
|
||||||
|
const [importBackupId, setImportBackupId] = useState<string>("");
|
||||||
|
const [selectedImportFile, setSelectedImportFile] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
loadConfigPath();
|
loadConfigPath();
|
||||||
@@ -346,6 +355,66 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 导出配置处理函数
|
||||||
|
const handleExportConfig = async () => {
|
||||||
|
try {
|
||||||
|
const defaultName = `cc-switch-config-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
const filePath = await window.api.saveFileDialog(defaultName);
|
||||||
|
|
||||||
|
if (!filePath) return; // 用户取消了
|
||||||
|
|
||||||
|
const result = await window.api.exportConfigToFile(filePath);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`${t("settings.configExported")}\n${result.filePath}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导出配置失败:", error);
|
||||||
|
alert(`${t("settings.exportFailed")}: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择要导入的文件
|
||||||
|
const handleSelectImportFile = async () => {
|
||||||
|
try {
|
||||||
|
const filePath = await window.api.openFileDialog();
|
||||||
|
if (filePath) {
|
||||||
|
setSelectedImportFile(filePath);
|
||||||
|
setImportStatus('idle'); // 重置状态
|
||||||
|
setImportError('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择文件失败:', error);
|
||||||
|
alert(`${t("settings.selectFileFailed")}: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行导入
|
||||||
|
const handleExecuteImport = async () => {
|
||||||
|
if (!selectedImportFile || isImporting) return;
|
||||||
|
|
||||||
|
setIsImporting(true);
|
||||||
|
setImportStatus('importing');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.api.importConfigFromFile(selectedImportFile);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setImportBackupId(result.backupId || '');
|
||||||
|
setImportStatus('success');
|
||||||
|
// ImportProgressModal 会在2秒后触发数据刷新回调
|
||||||
|
} else {
|
||||||
|
setImportError(result.message || t("settings.configCorrupted"));
|
||||||
|
setImportStatus('error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setImportError(String(error));
|
||||||
|
setImportStatus('error');
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
@@ -435,8 +504,6 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* VS Code 自动同步设置已移除 */}
|
|
||||||
|
|
||||||
{/* 配置文件位置 */}
|
{/* 配置文件位置 */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
@@ -544,6 +611,56 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 导入导出 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
{t("settings.importExport")}
|
||||||
|
</h3>
|
||||||
|
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 导出按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={handleExportConfig}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium rounded-lg transition-colors bg-gray-500 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-700 text-white"
|
||||||
|
>
|
||||||
|
<Save size={12} />
|
||||||
|
{t("settings.exportConfig")}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 导入区域 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSelectImportFile}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium rounded-lg transition-colors bg-gray-500 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-700 text-white"
|
||||||
|
>
|
||||||
|
<FolderOpen size={12} />
|
||||||
|
{t("settings.selectConfigFile")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleExecuteImport}
|
||||||
|
disabled={!selectedImportFile || isImporting}
|
||||||
|
className={`px-3 py-2 text-xs font-medium rounded-lg transition-colors text-white ${
|
||||||
|
!selectedImportFile || isImporting
|
||||||
|
? 'bg-gray-400 cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isImporting ? t("settings.importing") : t("settings.import")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 显示选择的文件 */}
|
||||||
|
{selectedImportFile && (
|
||||||
|
<div className="text-xs text-gray-600 dark:text-gray-400 px-2 py-1 bg-gray-50 dark:bg-gray-900 rounded break-all">
|
||||||
|
{selectedImportFile.split('/').pop() || selectedImportFile.split('\\').pop() || selectedImportFile}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 关于 */}
|
{/* 关于 */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
@@ -638,6 +755,28 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Import Progress Modal */}
|
||||||
|
{importStatus !== 'idle' && (
|
||||||
|
<ImportProgressModal
|
||||||
|
status={importStatus}
|
||||||
|
message={importError}
|
||||||
|
backupId={importBackupId}
|
||||||
|
onComplete={() => {
|
||||||
|
setImportStatus('idle');
|
||||||
|
setImportError('');
|
||||||
|
setSelectedImportFile('');
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
if (onImportSuccess) {
|
||||||
|
void onImportSuccess();
|
||||||
|
}
|
||||||
|
void window.api
|
||||||
|
.updateTrayMenu()
|
||||||
|
.catch((error) => console.error("[SettingsModal] Failed to refresh tray menu", error));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export interface CodexProviderPreset {
|
|||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
category?: ProviderCategory; // 新增:分类
|
category?: ProviderCategory; // 新增:分类
|
||||||
isCustomTemplate?: boolean; // 标识是否为自定义模板
|
isCustomTemplate?: boolean; // 标识是否为自定义模板
|
||||||
|
// 新增:请求地址候选列表(用于地址管理/测速)
|
||||||
|
endpointCandidates?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,5 +73,11 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
|||||||
"https://codex-api.packycode.com/v1",
|
"https://codex-api.packycode.com/v1",
|
||||||
"gpt-5-codex"
|
"gpt-5-codex"
|
||||||
),
|
),
|
||||||
|
// Codex 请求地址候选(用于地址管理/测速)
|
||||||
|
endpointCandidates: [
|
||||||
|
"https://codex-api.packycode.com/v1",
|
||||||
|
"https://codex-api-hk-cn2.packycode.com/v1",
|
||||||
|
"https://codex-api-hk-cdn.packycode.com/v1",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
*/
|
*/
|
||||||
import { ProviderCategory } from "../types";
|
import { ProviderCategory } from "../types";
|
||||||
|
|
||||||
|
export interface TemplateValueConfig {
|
||||||
|
label: string;
|
||||||
|
placeholder: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
editorValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProviderPreset {
|
export interface ProviderPreset {
|
||||||
name: string;
|
name: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
@@ -11,6 +18,10 @@ export interface ProviderPreset {
|
|||||||
settingsConfig: object;
|
settingsConfig: object;
|
||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
category?: ProviderCategory; // 新增:分类
|
category?: ProviderCategory; // 新增:分类
|
||||||
|
// 新增:模板变量定义,用于动态替换配置中的值
|
||||||
|
templateValues?: Record<string, TemplateValueConfig>; // editorValue 存储编辑器中的实时输入值
|
||||||
|
// 新增:请求地址候选列表(用于地址管理/测速)
|
||||||
|
endpointCandidates?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const providerPresets: ProviderPreset[] = [
|
export const providerPresets: ProviderPreset[] = [
|
||||||
@@ -43,8 +54,12 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "GLM-4.5",
|
// 兼容旧键名,保持前端读取一致
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "GLM-4.5-Air",
|
ANTHROPIC_MODEL: "GLM-4.6",
|
||||||
|
ANTHROPIC_SMALL_FAST_MODEL: "glm-4.5-air",
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.5-air",
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.6",
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -99,6 +114,36 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 请求地址候选(用于地址管理/测速)
|
||||||
|
endpointCandidates: [
|
||||||
|
"https://api.packycode.com",
|
||||||
|
"https://api-hk-cn2.packycode.com",
|
||||||
|
"https://api-hk-g.packycode.com",
|
||||||
|
"https://api-us-cn2.packycode.com",
|
||||||
|
"https://api-cf-pro.packycode.com",
|
||||||
|
],
|
||||||
category: "third_party",
|
category: "third_party",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "KAT-Coder 官方",
|
||||||
|
websiteUrl: "https://console.streamlake.ai/wanqing/",
|
||||||
|
apiKeyUrl: "https://console.streamlake.ai/console/wanqing/api-key",
|
||||||
|
settingsConfig: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy",
|
||||||
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
|
ANTHROPIC_MODEL: "KAT-Coder",
|
||||||
|
ANTHROPIC_SMALL_FAST_MODEL: "KAT-Coder",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: "cn_official",
|
||||||
|
templateValues: {
|
||||||
|
ENDPOINT_ID: {
|
||||||
|
label: "Vanchin Endpoint ID",
|
||||||
|
placeholder: "ep-xxx-xxx",
|
||||||
|
defaultValue: "",
|
||||||
|
editorValue: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
|
|
||||||
const VSCODE_AUTO_SYNC_KEY = "vscode-auto-sync-enabled";
|
|
||||||
const VSCODE_AUTO_SYNC_EVENT = "vscode-auto-sync-changed";
|
|
||||||
|
|
||||||
export function useVSCodeAutoSync() {
|
|
||||||
// 默认开启自动同步;若本地存储存在记录,则以记录为准
|
|
||||||
const [isAutoSyncEnabled, setIsAutoSyncEnabled] = useState<boolean>(true);
|
|
||||||
|
|
||||||
// 从 localStorage 读取初始状态
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(VSCODE_AUTO_SYNC_KEY);
|
|
||||||
if (saved !== null) {
|
|
||||||
setIsAutoSyncEnabled(saved === "true");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("读取自动同步状态失败:", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 订阅同窗口的自定义事件,以及跨窗口的 storage 事件,实现全局同步
|
|
||||||
useEffect(() => {
|
|
||||||
const onCustom = (e: Event) => {
|
|
||||||
try {
|
|
||||||
const detail = (e as CustomEvent).detail as
|
|
||||||
| { enabled?: boolean }
|
|
||||||
| undefined;
|
|
||||||
if (detail && typeof detail.enabled === "boolean") {
|
|
||||||
setIsAutoSyncEnabled(detail.enabled);
|
|
||||||
} else {
|
|
||||||
// 兜底:从 localStorage 读取
|
|
||||||
const saved = localStorage.getItem(VSCODE_AUTO_SYNC_KEY);
|
|
||||||
if (saved !== null) setIsAutoSyncEnabled(saved === "true");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 忽略
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onStorage = (e: StorageEvent) => {
|
|
||||||
if (e.key === VSCODE_AUTO_SYNC_KEY) {
|
|
||||||
setIsAutoSyncEnabled(e.newValue === "true");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener(VSCODE_AUTO_SYNC_EVENT, onCustom as EventListener);
|
|
||||||
window.addEventListener("storage", onStorage);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener(
|
|
||||||
VSCODE_AUTO_SYNC_EVENT,
|
|
||||||
onCustom as EventListener,
|
|
||||||
);
|
|
||||||
window.removeEventListener("storage", onStorage);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 启用自动同步
|
|
||||||
const enableAutoSync = useCallback(() => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(VSCODE_AUTO_SYNC_KEY, "true");
|
|
||||||
setIsAutoSyncEnabled(true);
|
|
||||||
// 通知同窗口其他订阅者
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent(VSCODE_AUTO_SYNC_EVENT, { detail: { enabled: true } }),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("保存自动同步状态失败:", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 禁用自动同步
|
|
||||||
const disableAutoSync = useCallback(() => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(VSCODE_AUTO_SYNC_KEY, "false");
|
|
||||||
setIsAutoSyncEnabled(false);
|
|
||||||
// 通知同窗口其他订阅者
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent(VSCODE_AUTO_SYNC_EVENT, { detail: { enabled: false } }),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("保存自动同步状态失败:", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 切换自动同步状态
|
|
||||||
const toggleAutoSync = useCallback(() => {
|
|
||||||
if (isAutoSyncEnabled) {
|
|
||||||
disableAutoSync();
|
|
||||||
} else {
|
|
||||||
enableAutoSync();
|
|
||||||
}
|
|
||||||
}, [isAutoSyncEnabled, enableAutoSync, disableAutoSync]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isAutoSyncEnabled,
|
|
||||||
enableAutoSync,
|
|
||||||
disableAutoSync,
|
|
||||||
toggleAutoSync,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -38,8 +38,6 @@
|
|||||||
"addNewProvider": "Add New Provider",
|
"addNewProvider": "Add New Provider",
|
||||||
"configError": "Configuration Error",
|
"configError": "Configuration Error",
|
||||||
"notConfigured": "Not configured for official website",
|
"notConfigured": "Not configured for official website",
|
||||||
"applyToVSCode": "Apply to VS Code",
|
|
||||||
"removeFromVSCode": "Remove from VS Code",
|
|
||||||
"applyToClaudePlugin": "Apply to Claude plugin",
|
"applyToClaudePlugin": "Apply to Claude plugin",
|
||||||
"removeFromClaudePlugin": "Remove from Claude plugin"
|
"removeFromClaudePlugin": "Remove from Claude plugin"
|
||||||
},
|
},
|
||||||
@@ -49,14 +47,8 @@
|
|||||||
"switchSuccess": "Switch successful! Please restart {{appName}} terminal to take effect",
|
"switchSuccess": "Switch successful! Please restart {{appName}} terminal to take effect",
|
||||||
"switchFailed": "Switch failed, please check configuration",
|
"switchFailed": "Switch failed, please check configuration",
|
||||||
"autoImported": "Default provider created from existing configuration",
|
"autoImported": "Default provider created from existing configuration",
|
||||||
"appliedToVSCode": "Applied to VS Code, restart Codex plugin to take effect",
|
|
||||||
"removedFromVSCode": "Removed from VS Code, restart Codex plugin to take effect",
|
|
||||||
"syncedToVSCode": "Synced to VS Code",
|
|
||||||
"vscodeSettingsNotFound": "VS Code user settings file (settings.json) not found",
|
|
||||||
"missingBaseUrl": "Current configuration missing base_url, cannot write to VS Code",
|
|
||||||
"saveFailed": "Save failed: {{error}}",
|
"saveFailed": "Save failed: {{error}}",
|
||||||
"saveFailedGeneric": "Save failed, please try again",
|
"saveFailedGeneric": "Save failed, please try again",
|
||||||
"syncVSCodeFailed": "Sync to VS Code failed",
|
|
||||||
"appliedToClaudePlugin": "Applied to Claude plugin",
|
"appliedToClaudePlugin": "Applied to Claude plugin",
|
||||||
"removedFromClaudePlugin": "Removed from Claude plugin",
|
"removedFromClaudePlugin": "Removed from Claude plugin",
|
||||||
"syncClaudePluginFailed": "Sync Claude plugin failed"
|
"syncClaudePluginFailed": "Sync Claude plugin failed"
|
||||||
@@ -69,6 +61,19 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"importExport": "Import/Export Config",
|
||||||
|
"exportConfig": "Export Config to File",
|
||||||
|
"selectConfigFile": "Select Config File",
|
||||||
|
"import": "Import",
|
||||||
|
"importing": "Importing...",
|
||||||
|
"importSuccess": "Import Successful!",
|
||||||
|
"importFailed": "Import Failed",
|
||||||
|
"configExported": "Config exported to:",
|
||||||
|
"exportFailed": "Export failed",
|
||||||
|
"selectFileFailed": "Failed to select file",
|
||||||
|
"configCorrupted": "Config file may be corrupted or invalid",
|
||||||
|
"backupId": "Backup ID",
|
||||||
|
"autoReload": "Data will refresh automatically in 2 seconds...",
|
||||||
"languageOptionChinese": "中文",
|
"languageOptionChinese": "中文",
|
||||||
"languageOptionEnglish": "English",
|
"languageOptionEnglish": "English",
|
||||||
"windowBehavior": "Window Behavior",
|
"windowBehavior": "Window Behavior",
|
||||||
@@ -101,7 +106,6 @@
|
|||||||
"providerSwitchReceived": "Received provider switch event:",
|
"providerSwitchReceived": "Received provider switch event:",
|
||||||
"setupListenerFailed": "Failed to setup provider switch listener:",
|
"setupListenerFailed": "Failed to setup provider switch listener:",
|
||||||
"updateProviderFailed": "Update provider failed:",
|
"updateProviderFailed": "Update provider failed:",
|
||||||
"syncToVSCodeFailed": "Sync to VS Code failed:",
|
|
||||||
"autoImportFailed": "Auto import default configuration failed:",
|
"autoImportFailed": "Auto import default configuration failed:",
|
||||||
"openLinkFailed": "Failed to open link:",
|
"openLinkFailed": "Failed to open link:",
|
||||||
"getVersionFailed": "Failed to get version info:",
|
"getVersionFailed": "Failed to get version info:",
|
||||||
|
|||||||
@@ -38,8 +38,6 @@
|
|||||||
"addNewProvider": "添加新供应商",
|
"addNewProvider": "添加新供应商",
|
||||||
"configError": "配置错误",
|
"configError": "配置错误",
|
||||||
"notConfigured": "未配置官网地址",
|
"notConfigured": "未配置官网地址",
|
||||||
"applyToVSCode": "应用到 VS Code",
|
|
||||||
"removeFromVSCode": "从 VS Code 移除",
|
|
||||||
"applyToClaudePlugin": "应用到 Claude 插件",
|
"applyToClaudePlugin": "应用到 Claude 插件",
|
||||||
"removeFromClaudePlugin": "从 Claude 插件移除"
|
"removeFromClaudePlugin": "从 Claude 插件移除"
|
||||||
},
|
},
|
||||||
@@ -49,14 +47,8 @@
|
|||||||
"switchSuccess": "切换成功!请重启 {{appName}} 终端以生效",
|
"switchSuccess": "切换成功!请重启 {{appName}} 终端以生效",
|
||||||
"switchFailed": "切换失败,请检查配置",
|
"switchFailed": "切换失败,请检查配置",
|
||||||
"autoImported": "已从现有配置创建默认供应商",
|
"autoImported": "已从现有配置创建默认供应商",
|
||||||
"appliedToVSCode": "已应用到 VS Code,重启 Codex 插件以生效",
|
|
||||||
"removedFromVSCode": "已从 VS Code 移除,重启 Codex 插件以生效",
|
|
||||||
"syncedToVSCode": "已同步到 VS Code",
|
|
||||||
"vscodeSettingsNotFound": "未找到 VS Code 用户设置文件 (settings.json)",
|
|
||||||
"missingBaseUrl": "当前配置缺少 base_url,无法写入 VS Code",
|
|
||||||
"saveFailed": "保存失败:{{error}}",
|
"saveFailed": "保存失败:{{error}}",
|
||||||
"saveFailedGeneric": "保存失败,请重试",
|
"saveFailedGeneric": "保存失败,请重试",
|
||||||
"syncVSCodeFailed": "同步 VS Code 失败",
|
|
||||||
"appliedToClaudePlugin": "已应用到 Claude 插件",
|
"appliedToClaudePlugin": "已应用到 Claude 插件",
|
||||||
"removedFromClaudePlugin": "已从 Claude 插件移除",
|
"removedFromClaudePlugin": "已从 Claude 插件移除",
|
||||||
"syncClaudePluginFailed": "同步 Claude 插件失败"
|
"syncClaudePluginFailed": "同步 Claude 插件失败"
|
||||||
@@ -69,6 +61,19 @@
|
|||||||
"title": "设置",
|
"title": "设置",
|
||||||
"general": "通用",
|
"general": "通用",
|
||||||
"language": "界面语言",
|
"language": "界面语言",
|
||||||
|
"importExport": "导入导出配置",
|
||||||
|
"exportConfig": "导出配置到文件",
|
||||||
|
"selectConfigFile": "选择配置文件",
|
||||||
|
"import": "导入",
|
||||||
|
"importing": "导入中...",
|
||||||
|
"importSuccess": "导入成功!",
|
||||||
|
"importFailed": "导入失败",
|
||||||
|
"configExported": "配置已导出到:",
|
||||||
|
"exportFailed": "导出失败",
|
||||||
|
"selectFileFailed": "选择文件失败",
|
||||||
|
"configCorrupted": "配置文件可能已损坏或格式不正确",
|
||||||
|
"backupId": "备份ID",
|
||||||
|
"autoReload": "数据将在2秒后自动刷新...",
|
||||||
"languageOptionChinese": "中文",
|
"languageOptionChinese": "中文",
|
||||||
"languageOptionEnglish": "English",
|
"languageOptionEnglish": "English",
|
||||||
"windowBehavior": "窗口行为",
|
"windowBehavior": "窗口行为",
|
||||||
@@ -101,7 +106,6 @@
|
|||||||
"providerSwitchReceived": "收到供应商切换事件:",
|
"providerSwitchReceived": "收到供应商切换事件:",
|
||||||
"setupListenerFailed": "设置供应商切换监听器失败:",
|
"setupListenerFailed": "设置供应商切换监听器失败:",
|
||||||
"updateProviderFailed": "更新供应商失败:",
|
"updateProviderFailed": "更新供应商失败:",
|
||||||
"syncToVSCodeFailed": "同步到VS Code失败:",
|
|
||||||
"autoImportFailed": "自动导入默认配置失败:",
|
"autoImportFailed": "自动导入默认配置失败:",
|
||||||
"openLinkFailed": "打开链接失败:",
|
"openLinkFailed": "打开链接失败:",
|
||||||
"getVersionFailed": "获取版本信息失败:",
|
"getVersionFailed": "获取版本信息失败:",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { Provider, Settings } from "../types";
|
import { Provider, Settings, CustomEndpoint } from "../types";
|
||||||
|
|
||||||
// 应用类型
|
// 应用类型
|
||||||
export type AppType = "claude" | "codex";
|
export type AppType = "claude" | "codex";
|
||||||
@@ -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 = {
|
||||||
// 获取所有供应商
|
// 获取所有供应商
|
||||||
@@ -132,40 +139,22 @@ export const tauriAPI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取 Claude Code 配置状态
|
// 打开配置目录(按应用类型)
|
||||||
getClaudeConfigStatus: async (): Promise<ConfigStatus> => {
|
|
||||||
try {
|
|
||||||
return await invoke("get_claude_config_status");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取配置状态失败:", error);
|
|
||||||
return {
|
|
||||||
exists: false,
|
|
||||||
path: "",
|
|
||||||
error: String(error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取应用配置状态(通用)
|
|
||||||
getConfigStatus: async (app?: AppType): Promise<ConfigStatus> => {
|
|
||||||
try {
|
|
||||||
return await invoke("get_config_status", { app_type: app, app });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取配置状态失败:", error);
|
|
||||||
return {
|
|
||||||
exists: false,
|
|
||||||
path: "",
|
|
||||||
error: String(error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 打开配置文件夹
|
|
||||||
openConfigFolder: async (app?: AppType): Promise<void> => {
|
openConfigFolder: async (app?: AppType): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await invoke("open_config_folder", { app_type: app, app });
|
await invoke("open_config_folder", { app_type: app, app });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("打开配置文件夹失败:", error);
|
console.error("打开配置目录失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择配置目录(可选默认路径)
|
||||||
|
selectConfigDirectory: async (defaultPath?: string): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
return await invoke("pick_directory", { defaultPath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("选择配置目录失败:", error);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -181,49 +170,20 @@ export const tauriAPI = {
|
|||||||
// 更新托盘菜单
|
// 更新托盘菜单
|
||||||
updateTrayMenu: async (): Promise<boolean> => {
|
updateTrayMenu: async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
return await invoke("update_tray_menu");
|
return await invoke<boolean>("update_tray_menu");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("更新托盘菜单失败:", error);
|
console.error("更新托盘菜单失败:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 监听供应商切换事件
|
// 获取应用设置
|
||||||
onProviderSwitched: async (
|
|
||||||
callback: (data: { appType: string; providerId: string }) => void,
|
|
||||||
): Promise<UnlistenFn> => {
|
|
||||||
return await listen("provider-switched", (event) => {
|
|
||||||
callback(event.payload as { appType: string; providerId: string });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// (保留空位,取消迁移提示)
|
|
||||||
|
|
||||||
// 选择配置目录
|
|
||||||
selectConfigDirectory: async (
|
|
||||||
defaultPath?: string,
|
|
||||||
): Promise<string | null> => {
|
|
||||||
try {
|
|
||||||
const sanitized =
|
|
||||||
defaultPath && defaultPath.trim() !== ""
|
|
||||||
? defaultPath
|
|
||||||
: undefined;
|
|
||||||
return await invoke<string | null>("pick_directory", {
|
|
||||||
defaultPath: sanitized,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("选择配置目录失败:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取设置
|
|
||||||
getSettings: async (): Promise<Settings> => {
|
getSettings: async (): Promise<Settings> => {
|
||||||
try {
|
try {
|
||||||
return await invoke("get_settings");
|
return await invoke("get_settings");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取设置失败:", error);
|
console.error("获取设置失败:", error);
|
||||||
return { showInTray: true, minimizeToTrayOnClose: true };
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -275,38 +235,6 @@ export const tauriAPI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// VS Code: 获取 settings.json 状态
|
|
||||||
getVSCodeSettingsStatus: async (): Promise<{
|
|
||||||
exists: boolean;
|
|
||||||
path: string;
|
|
||||||
error?: string;
|
|
||||||
}> => {
|
|
||||||
try {
|
|
||||||
return await invoke("get_vscode_settings_status");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取 VS Code 设置状态失败:", error);
|
|
||||||
return { exists: false, path: "", error: String(error) };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// VS Code: 读取 settings.json 文本
|
|
||||||
readVSCodeSettings: async (): Promise<string> => {
|
|
||||||
try {
|
|
||||||
return await invoke("read_vscode_settings");
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`读取 VS Code 设置失败: ${String(error)}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// VS Code: 写回 settings.json 文本(不自动创建)
|
|
||||||
writeVSCodeSettings: async (content: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
return await invoke("write_vscode_settings", { content });
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`写入 VS Code 设置失败: ${String(error)}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Claude 插件:获取 ~/.claude/config.json 状态
|
// Claude 插件:获取 ~/.claude/config.json 状态
|
||||||
getClaudePluginStatus: async (): Promise<ConfigStatus> => {
|
getClaudePluginStatus: async (): Promise<ConfigStatus> => {
|
||||||
try {
|
try {
|
||||||
@@ -346,6 +274,175 @@ export const tauriAPI = {
|
|||||||
throw new Error(`检测 Claude 插件配置失败: ${String(error)}`);
|
throw new Error(`检测 Claude 插件配置失败: ${String(error)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ours: 第三方/自定义供应商——测速与端点管理
|
||||||
|
// 第三方/自定义供应商:批量测试端点延迟
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取自定义端点列表
|
||||||
|
getCustomEndpoints: async (
|
||||||
|
appType: AppType,
|
||||||
|
providerId: string,
|
||||||
|
): Promise<CustomEndpoint[]> => {
|
||||||
|
try {
|
||||||
|
return await invoke<CustomEndpoint[]>("get_custom_endpoints", {
|
||||||
|
// 兼容不同后端参数命名
|
||||||
|
app_type: appType,
|
||||||
|
app: appType,
|
||||||
|
appType: appType,
|
||||||
|
provider_id: providerId,
|
||||||
|
providerId: providerId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取自定义端点列表失败:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加自定义端点
|
||||||
|
addCustomEndpoint: async (
|
||||||
|
appType: AppType,
|
||||||
|
providerId: string,
|
||||||
|
url: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await invoke("add_custom_endpoint", {
|
||||||
|
app_type: appType,
|
||||||
|
app: appType,
|
||||||
|
appType: appType,
|
||||||
|
provider_id: providerId,
|
||||||
|
providerId: providerId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("添加自定义端点失败:", error);
|
||||||
|
// 尽量抛出可读信息
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
throw new Error(String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除自定义端点
|
||||||
|
removeCustomEndpoint: async (
|
||||||
|
appType: AppType,
|
||||||
|
providerId: string,
|
||||||
|
url: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await invoke("remove_custom_endpoint", {
|
||||||
|
app_type: appType,
|
||||||
|
app: appType,
|
||||||
|
appType: appType,
|
||||||
|
provider_id: providerId,
|
||||||
|
providerId: providerId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除自定义端点失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新端点最后使用时间
|
||||||
|
updateEndpointLastUsed: async (
|
||||||
|
appType: AppType,
|
||||||
|
providerId: string,
|
||||||
|
url: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await invoke("update_endpoint_last_used", {
|
||||||
|
app_type: appType,
|
||||||
|
app: appType,
|
||||||
|
appType: appType,
|
||||||
|
provider_id: providerId,
|
||||||
|
providerId: providerId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("更新端点最后使用时间失败:", error);
|
||||||
|
// 不抛出错误,因为这不是关键操作
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// theirs: 导入导出与文件对话框
|
||||||
|
// 导出配置到文件
|
||||||
|
exportConfigToFile: async (filePath: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
filePath: string;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
return await invoke("export_config_to_file", { filePath });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`导出配置失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从文件导入配置
|
||||||
|
importConfigFromFile: async (filePath: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
backupId?: string;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
return await invoke("import_config_from_file", { filePath });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`导入配置失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存文件对话框
|
||||||
|
saveFileDialog: async (defaultName: string): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const result = await invoke<string | null>("save_file_dialog", { defaultName });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("打开保存对话框失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 打开文件对话框
|
||||||
|
openFileDialog: async (): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const result = await invoke<string | null>("open_file_dialog");
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("打开文件对话框失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 监听供应商切换事件
|
||||||
|
onProviderSwitched: async (
|
||||||
|
callback: (data: { appType: string; providerId: string }) => void,
|
||||||
|
): Promise<UnlistenFn> => {
|
||||||
|
const unlisten = await listen("provider-switched", (event) => {
|
||||||
|
try {
|
||||||
|
// 事件 payload 形如 { appType: string, providerId: string }
|
||||||
|
callback(event.payload as any);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("处理 provider-switched 事件失败: ", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return unlisten;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建全局 API 对象,兼容现有代码
|
// 创建全局 API 对象,兼容现有代码
|
||||||
|
|||||||
19
src/types.ts
19
src/types.ts
@@ -13,6 +13,8 @@ export interface Provider {
|
|||||||
// 新增:供应商分类(用于差异化提示/能力开关)
|
// 新增:供应商分类(用于差异化提示/能力开关)
|
||||||
category?: ProviderCategory;
|
category?: ProviderCategory;
|
||||||
createdAt?: number; // 添加时间戳(毫秒)
|
createdAt?: number; // 添加时间戳(毫秒)
|
||||||
|
// 可选:供应商元数据(仅存于 ~/.cc-switch/config.json,不写入 live 配置)
|
||||||
|
meta?: ProviderMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
@@ -20,6 +22,19 @@ export interface AppConfig {
|
|||||||
current: string;
|
current: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自定义端点配置
|
||||||
|
export interface CustomEndpoint {
|
||||||
|
url: string;
|
||||||
|
addedAt: number;
|
||||||
|
lastUsed?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 供应商元数据(字段名与后端一致,保持 snake_case)
|
||||||
|
export interface ProviderMeta {
|
||||||
|
// 自定义端点:以 URL 为键,值为端点信息
|
||||||
|
custom_endpoints?: Record<string, CustomEndpoint>;
|
||||||
|
}
|
||||||
|
|
||||||
// 应用设置类型(用于 SettingsModal 与 Tauri API)
|
// 应用设置类型(用于 SettingsModal 与 Tauri API)
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
// 是否在系统托盘(macOS 菜单栏)显示图标
|
// 是否在系统托盘(macOS 菜单栏)显示图标
|
||||||
@@ -32,4 +47,8 @@ export interface Settings {
|
|||||||
codexConfigDir?: string;
|
codexConfigDir?: string;
|
||||||
// 首选语言(可选,默认中文)
|
// 首选语言(可选,默认中文)
|
||||||
language?: "en" | "zh";
|
language?: "en" | "zh";
|
||||||
|
// Claude 自定义端点列表
|
||||||
|
customEndpointsClaude?: Record<string, CustomEndpoint>;
|
||||||
|
// Codex 自定义端点列表
|
||||||
|
customEndpointsCodex?: Record<string, CustomEndpoint>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// 供应商配置处理工具函数
|
// 供应商配置处理工具函数
|
||||||
|
|
||||||
|
import type { TemplateValueConfig } from "../config/providerPresets";
|
||||||
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
||||||
return Object.prototype.toString.call(value) === "[object Object]";
|
return Object.prototype.toString.call(value) === "[object Object]";
|
||||||
};
|
};
|
||||||
@@ -173,6 +175,51 @@ export const getApiKeyFromConfig = (jsonString: string): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 模板变量替换
|
||||||
|
export const applyTemplateValues = (
|
||||||
|
config: any,
|
||||||
|
templateValues: Record<string, TemplateValueConfig> | undefined
|
||||||
|
): any => {
|
||||||
|
const resolvedValues = Object.fromEntries(
|
||||||
|
Object.entries(templateValues ?? {}).map(([key, value]) => {
|
||||||
|
const resolvedValue =
|
||||||
|
value.editorValue !== undefined
|
||||||
|
? value.editorValue
|
||||||
|
: value.defaultValue ?? "";
|
||||||
|
return [key, resolvedValue];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const replaceInString = (str: string): string => {
|
||||||
|
return Object.entries(resolvedValues).reduce((acc, [key, value]) => {
|
||||||
|
const placeholder = `\${${key}}`;
|
||||||
|
if (!acc.includes(placeholder)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
return acc.split(placeholder).join(value ?? "");
|
||||||
|
}, str);
|
||||||
|
};
|
||||||
|
|
||||||
|
const traverse = (obj: any): any => {
|
||||||
|
if (typeof obj === "string") {
|
||||||
|
return replaceInString(obj);
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(traverse);
|
||||||
|
}
|
||||||
|
if (obj && typeof obj === "object") {
|
||||||
|
const result: any = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
result[key] = traverse(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
return traverse(config);
|
||||||
|
};
|
||||||
|
|
||||||
// 判断配置中是否存在 API Key 字段
|
// 判断配置中是否存在 API Key 字段
|
||||||
export const hasApiKeyField = (jsonString: string): boolean => {
|
export const hasApiKeyField = (jsonString: string): boolean => {
|
||||||
try {
|
try {
|
||||||
@@ -318,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`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
||||||
|
|
||||||
const fmt = { insertSpaces: true, tabSize: 2, eol: "\n" } as const;
|
|
||||||
|
|
||||||
export interface AppliedCheck {
|
|
||||||
hasApiBase: boolean;
|
|
||||||
apiBase?: string;
|
|
||||||
hasPreferredAuthMethod: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeBaseUrl(url: string): string {
|
|
||||||
return url.replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDocEmpty = (s: string) => s.trim().length === 0;
|
|
||||||
|
|
||||||
// 检查 settings.json(JSONC 文本)中是否已经应用了我们的键
|
|
||||||
export function detectApplied(content: string): AppliedCheck {
|
|
||||||
try {
|
|
||||||
// 允许 JSONC 的宽松解析:jsonc-parser 的 parse 可以直接处理注释
|
|
||||||
const data = parse(content) as any;
|
|
||||||
const apiBase = data?.["chatgpt.apiBase"];
|
|
||||||
const method = data?.["chatgpt.config"]?.preferred_auth_method;
|
|
||||||
return {
|
|
||||||
hasApiBase: typeof apiBase === "string",
|
|
||||||
apiBase,
|
|
||||||
hasPreferredAuthMethod: typeof method === "string",
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return { hasApiBase: false, hasPreferredAuthMethod: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成“清理我们管理的键”后的文本(仅删除我们写入的两个键)
|
|
||||||
export function removeManagedKeys(content: string): string {
|
|
||||||
if (isDocEmpty(content)) return content; // 空文档无需删除
|
|
||||||
let out = content;
|
|
||||||
// 删除 chatgpt.apiBase
|
|
||||||
try {
|
|
||||||
out = applyEdits(
|
|
||||||
out,
|
|
||||||
modify(out, ["chatgpt.apiBase"], undefined, { formattingOptions: fmt }),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// 忽略删除失败
|
|
||||||
}
|
|
||||||
// 删除 chatgpt.config.preferred_auth_method(注意 chatgpt.config 是顶层带点的键)
|
|
||||||
try {
|
|
||||||
out = applyEdits(
|
|
||||||
out,
|
|
||||||
modify(out, ["chatgpt.config", "preferred_auth_method"], undefined, {
|
|
||||||
formattingOptions: fmt,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// 忽略删除失败
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容早期错误写入:若曾写成嵌套 chatgpt.config.preferred_auth_method,也一并清理
|
|
||||||
try {
|
|
||||||
out = applyEdits(
|
|
||||||
out,
|
|
||||||
modify(out, ["chatgpt", "config", "preferred_auth_method"], undefined, {
|
|
||||||
formattingOptions: fmt,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// 忽略删除失败
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理 chatgpt.config 的异常情况:
|
|
||||||
// 1. 早期遗留的标量值(字符串/数字/null等)
|
|
||||||
// 2. 空对象
|
|
||||||
// 3. 数组类型
|
|
||||||
try {
|
|
||||||
const data = parse(out) as any;
|
|
||||||
const cfg = data?.["chatgpt.config"];
|
|
||||||
|
|
||||||
// 需要清理的情况:
|
|
||||||
// - 标量值(null、字符串、数字等)
|
|
||||||
// - 数组
|
|
||||||
// - 空对象
|
|
||||||
const shouldRemove = cfg !== undefined && (
|
|
||||||
cfg === null ||
|
|
||||||
typeof cfg !== "object" ||
|
|
||||||
Array.isArray(cfg) ||
|
|
||||||
(typeof cfg === "object" && Object.keys(cfg).length === 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldRemove) {
|
|
||||||
out = applyEdits(
|
|
||||||
out,
|
|
||||||
modify(out, ["chatgpt.config"], undefined, { formattingOptions: fmt }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 忽略解析失败,保持已删除的键
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成“应用供应商到 VS Code”后的文本:
|
|
||||||
// - 先清理我们管理的键
|
|
||||||
// - 再根据是否官方决定写入(官方:不写入;非官方:写入两个键)
|
|
||||||
export function applyProviderToVSCode(
|
|
||||||
content: string,
|
|
||||||
opts: { baseUrl?: string | null; isOfficial?: boolean },
|
|
||||||
): string {
|
|
||||||
let out = removeManagedKeys(content);
|
|
||||||
if (!opts.isOfficial && opts.baseUrl) {
|
|
||||||
const apiBase = normalizeBaseUrl(opts.baseUrl);
|
|
||||||
if (isDocEmpty(out)) {
|
|
||||||
// 简化:空文档直接写入新对象
|
|
||||||
const obj: any = {
|
|
||||||
"chatgpt.apiBase": apiBase,
|
|
||||||
"chatgpt.config": { preferred_auth_method: "apikey" },
|
|
||||||
};
|
|
||||||
out = JSON.stringify(obj, null, 2) + "\n";
|
|
||||||
} else {
|
|
||||||
out = applyEdits(
|
|
||||||
out,
|
|
||||||
modify(out, ["chatgpt.apiBase"], apiBase, { formattingOptions: fmt }),
|
|
||||||
);
|
|
||||||
out = applyEdits(
|
|
||||||
out,
|
|
||||||
modify(out, ["chatgpt.config", "preferred_auth_method"], "apikey", {
|
|
||||||
formattingOptions: fmt,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
47
src/vite-env.d.ts
vendored
47
src/vite-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
import { Provider, Settings } from "./types";
|
import { Provider, Settings, CustomEndpoint } from "./types";
|
||||||
import { AppType } from "./lib/tauri-api";
|
import { AppType } from "./lib/tauri-api";
|
||||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
@@ -29,6 +29,18 @@ declare global {
|
|||||||
getClaudeConfigStatus: () => Promise<ConfigStatus>;
|
getClaudeConfigStatus: () => Promise<ConfigStatus>;
|
||||||
getConfigStatus: (app?: AppType) => Promise<ConfigStatus>;
|
getConfigStatus: (app?: AppType) => Promise<ConfigStatus>;
|
||||||
getConfigDir: (app?: AppType) => Promise<string>;
|
getConfigDir: (app?: AppType) => Promise<string>;
|
||||||
|
saveFileDialog: (defaultName: string) => Promise<string | null>;
|
||||||
|
openFileDialog: () => Promise<string | null>;
|
||||||
|
exportConfigToFile: (filePath: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
filePath: string;
|
||||||
|
}>;
|
||||||
|
importConfigFromFile: (filePath: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
backupId?: string;
|
||||||
|
}>;
|
||||||
selectConfigDirectory: (defaultPath?: string) => Promise<string | null>;
|
selectConfigDirectory: (defaultPath?: string) => Promise<string | null>;
|
||||||
openConfigFolder: (app?: AppType) => Promise<void>;
|
openConfigFolder: (app?: AppType) => Promise<void>;
|
||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
@@ -42,10 +54,6 @@ declare global {
|
|||||||
isPortable: () => Promise<boolean>;
|
isPortable: () => Promise<boolean>;
|
||||||
getAppConfigPath: () => Promise<string>;
|
getAppConfigPath: () => Promise<string>;
|
||||||
openAppConfigFolder: () => Promise<void>;
|
openAppConfigFolder: () => Promise<void>;
|
||||||
// VS Code settings.json 能力
|
|
||||||
getVSCodeSettingsStatus: () => Promise<ConfigStatus>;
|
|
||||||
readVSCodeSettings: () => Promise<string>;
|
|
||||||
writeVSCodeSettings: (content: string) => Promise<boolean>;
|
|
||||||
// Claude 插件配置能力
|
// Claude 插件配置能力
|
||||||
getClaudePluginStatus: () => Promise<ConfigStatus>;
|
getClaudePluginStatus: () => Promise<ConfigStatus>;
|
||||||
readClaudePluginConfig: () => Promise<string | null>;
|
readClaudePluginConfig: () => Promise<string | null>;
|
||||||
@@ -53,6 +61,35 @@ 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;
|
||||||
|
}>>;
|
||||||
|
// 自定义端点管理
|
||||||
|
getCustomEndpoints: (
|
||||||
|
appType: AppType,
|
||||||
|
providerId: string
|
||||||
|
) => Promise<CustomEndpoint[]>;
|
||||||
|
addCustomEndpoint: (
|
||||||
|
appType: AppType,
|
||||||
|
providerId: string,
|
||||||
|
url: string
|
||||||
|
) => Promise<void>;
|
||||||
|
removeCustomEndpoint: (
|
||||||
|
appType: AppType,
|
||||||
|
providerId: string,
|
||||||
|
url: string
|
||||||
|
) => Promise<void>;
|
||||||
|
updateEndpointLastUsed: (
|
||||||
|
appType: AppType,
|
||||||
|
providerId: string,
|
||||||
|
url: string
|
||||||
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
platform: {
|
platform: {
|
||||||
isMac: boolean;
|
isMac: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user