From f3272b93a24d57d64773ff2c401c5802370f8b5c Mon Sep 17 00:00:00 2001 From: coso Date: Mon, 15 Dec 2025 09:30:10 +0800 Subject: [PATCH] fix: Kiro OAuth token refresh and health check - Fix health check request format with correct fields (chatTriggerType, modelId, origin) - Load clientIdHash file correctly for IdC authentication - Add success notifications for token refresh and health check - Clean up debug println statements, use tracing instead - Fix ESLint warnings Version: 0.6.1 --- .claude/settings.local.json | 5 +- package-lock.json | 14 +- package.json | 3 +- src-tauri/Cargo.lock | 463 +++++++++- src-tauri/Cargo.toml | 4 +- src-tauri/capabilities/default.json | 3 +- src-tauri/gen/schemas/acl-manifests.json | 2 +- src-tauri/gen/schemas/capabilities.json | 2 +- src-tauri/gen/schemas/desktop-schema.json | 66 ++ src-tauri/gen/schemas/macOS-schema.json | 66 ++ src-tauri/src/commands/mod.rs | 2 + src-tauri/src/commands/provider_pool_cmd.rs | 635 +++++++++++++ src-tauri/src/commands/route_cmd.rs | 92 ++ src-tauri/src/database/dao/mod.rs | 1 + src-tauri/src/database/dao/provider_pool.rs | 466 ++++++++++ src-tauri/src/database/schema.rs | 57 ++ src-tauri/src/lib.rs | 62 +- src-tauri/src/models/mod.rs | 3 + src-tauri/src/models/provider_pool_model.rs | 501 ++++++++++ src-tauri/src/models/route_model.rs | 135 +++ src-tauri/src/providers/claude_custom.rs | 156 ++++ src-tauri/src/providers/gemini.rs | 10 + src-tauri/src/providers/kiro.rs | 365 +++++++- src-tauri/src/providers/openai_custom.rs | 39 + src-tauri/src/providers/qwen.rs | 10 + src-tauri/src/server.rs | 799 +++++++++++++++- src-tauri/src/services/mod.rs | 2 + .../src/services/provider_pool_service.rs | 852 ++++++++++++++++++ src-tauri/src/services/token_cache_service.rs | 433 +++++++++ src-tauri/tauri.conf.json | 2 +- src/App.tsx | 10 +- src/components/Sidebar.tsx | 8 +- src/components/api-server/ApiServerPage.tsx | 7 +- src/components/api-server/RoutesTab.tsx | 300 ++++++ .../credentials/CredentialsPage.tsx | 411 --------- .../provider-pool/AddCredentialModal.tsx | 270 ++++++ .../provider-pool/CredentialCard.tsx | 401 +++++++++ .../provider-pool/EditCredentialModal.tsx | 349 +++++++ src/components/provider-pool/ErrorDisplay.tsx | 192 ++++ .../provider-pool/ProviderPoolPage.tsx | 379 ++++++++ src/components/provider-pool/index.ts | 4 + src/components/skills/RepoManagerPanel.tsx | 9 +- src/hooks/useProviderPool.ts | 196 ++++ src/lib/api/providerPool.ts | 270 ++++++ src/lib/api/routes.ts | 37 + 45 files changed, 7627 insertions(+), 466 deletions(-) create mode 100644 src-tauri/src/commands/provider_pool_cmd.rs create mode 100644 src-tauri/src/commands/route_cmd.rs create mode 100644 src-tauri/src/database/dao/provider_pool.rs create mode 100644 src-tauri/src/models/provider_pool_model.rs create mode 100644 src-tauri/src/models/route_model.rs create mode 100644 src-tauri/src/services/provider_pool_service.rs create mode 100644 src-tauri/src/services/token_cache_service.rs create mode 100644 src/components/api-server/RoutesTab.tsx delete mode 100644 src/components/credentials/CredentialsPage.tsx create mode 100644 src/components/provider-pool/AddCredentialModal.tsx create mode 100644 src/components/provider-pool/CredentialCard.tsx create mode 100644 src/components/provider-pool/EditCredentialModal.tsx create mode 100644 src/components/provider-pool/ErrorDisplay.tsx create mode 100644 src/components/provider-pool/ProviderPoolPage.tsx create mode 100644 src/components/provider-pool/index.ts create mode 100644 src/hooks/useProviderPool.ts create mode 100644 src/lib/api/providerPool.ts create mode 100644 src/lib/api/routes.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d62ec1bbc..f6fc03548 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,10 @@ "Bash(cargo check:*)", "Bash(rm:*)", "Bash(cargo clippy:*)", - "Bash(cargo fmt:*)" + "Bash(cargo fmt:*)", + "Bash(lsof:*)", + "Bash(xargs kill:*)", + "Bash(cargo run:*)" ], "deny": [], "ask": [] diff --git a/package-lock.json b/package-lock.json index 256b89152..a6be94df8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxycast", - "version": "0.3.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxycast", - "version": "0.3.0", + "version": "0.6.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", @@ -18,6 +18,7 @@ "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.3", "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-shell": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -2592,6 +2593,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.2.tgz", + "integrity": "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.3.tgz", diff --git a/package.json b/package.json index 38e4a3f7d..202416934 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.6.0", + "version": "0.6.1", "type": "module", "scripts": { "dev": "vite", @@ -22,6 +22,7 @@ "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.3", "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-shell": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 387947429..b91db6209 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -70,6 +70,50 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -484,6 +528,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -671,6 +724,19 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.5.5" @@ -780,6 +846,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0", + "block2 0.6.2", + "libc", "objc2 0.6.3", ] @@ -794,6 +862,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -817,6 +894,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -882,6 +965,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -909,6 +1019,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1077,6 +1208,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2225,6 +2369,19 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2609,6 +2766,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -2644,6 +2811,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2862,7 +3035,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.12.1", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -2980,12 +3153,13 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "async-stream", "axum", "chrono", + "dashmap", "dirs 5.0.1", "futures", "indexmap 2.12.1", @@ -2999,6 +3173,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-autostart", + "tauri-plugin-dialog", "tauri-plugin-shell", "tokio", "tracing", @@ -3008,6 +3183,15 @@ dependencies = [ "zip", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3057,6 +3241,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3077,6 +3271,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3095,6 +3299,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3242,6 +3455,31 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -3406,6 +3644,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3829,6 +4073,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.9" @@ -4155,6 +4405,46 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "toml 0.9.8", + "url", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.3" @@ -4405,6 +4695,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -4692,6 +4983,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -4966,6 +5268,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.10.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -5705,6 +6067,62 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.14", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.111", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.14", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.31" @@ -5833,3 +6251,44 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.14", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.111", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.111", + "winnow 0.7.14", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8d69ed96c..de31d548e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.6.0" +version = "0.6.1" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" @@ -16,6 +16,7 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } tauri-plugin-shell = "2" tauri-plugin-autostart = "2" +tauri-plugin-dialog = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } @@ -36,6 +37,7 @@ serde_yaml = "0.9" indexmap = { version = "2", features = ["serde"] } zip = "0.6" anyhow = "1" +dashmap = "5" [features] default = ["custom-protocol"] diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 834db0ca5..ecf94c399 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,6 +5,7 @@ "windows": ["main"], "permissions": [ "core:default", - "shell:allow-open" + "shell:allow-open", + "dialog:default" ] } diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index 69ed83adf..7f023fd90 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"autostart":{"default_permission":{"identifier":"default","description":"This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n","permissions":["allow-enable","allow-disable","allow-is-enabled"]},"permissions":{"allow-disable":{"identifier":"allow-disable","description":"Enables the disable command without any pre-configured scope.","commands":{"allow":["disable"],"deny":[]}},"allow-enable":{"identifier":"allow-enable","description":"Enables the enable command without any pre-configured scope.","commands":{"allow":["enable"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"deny-disable":{"identifier":"deny-disable","description":"Denies the disable command without any pre-configured scope.","commands":{"allow":[],"deny":["disable"]}},"deny-enable":{"identifier":"deny-enable","description":"Denies the enable command without any pre-configured scope.","commands":{"allow":[],"deny":["enable"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file +{"autostart":{"default_permission":{"identifier":"default","description":"This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n","permissions":["allow-enable","allow-disable","allow-is-enabled"]},"permissions":{"allow-disable":{"identifier":"allow-disable","description":"Enables the disable command without any pre-configured scope.","commands":{"allow":["disable"],"deny":[]}},"allow-enable":{"identifier":"allow-enable","description":"Enables the enable command without any pre-configured scope.","commands":{"allow":["enable"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"deny-disable":{"identifier":"deny-disable","description":"Denies the disable command without any pre-configured scope.","commands":{"allow":[],"deny":["disable"]}},"deny-enable":{"identifier":"deny-enable","description":"Denies the enable command without any pre-configured scope.","commands":{"allow":[],"deny":["enable"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 56cdb5b15..13b8dfd25 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Default capabilities for ProxyCast","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Default capabilities for ProxyCast","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:default"]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json index 292903497..0a45d9c6b 100644 --- a/src-tauri/gen/schemas/desktop-schema.json +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -2396,6 +2396,72 @@ "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, { "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", "type": "string", diff --git a/src-tauri/gen/schemas/macOS-schema.json b/src-tauri/gen/schemas/macOS-schema.json index 292903497..0a45d9c6b 100644 --- a/src-tauri/gen/schemas/macOS-schema.json +++ b/src-tauri/gen/schemas/macOS-schema.json @@ -2396,6 +2396,72 @@ "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, { "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", "type": "string", diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 8f91ff9d2..fad842d0b 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -2,5 +2,7 @@ pub mod config_cmd; pub mod mcp_cmd; pub mod oauth_cmd; pub mod prompt_cmd; +pub mod provider_pool_cmd; +pub mod route_cmd; pub mod skill_cmd; pub mod switch_cmd; diff --git a/src-tauri/src/commands/provider_pool_cmd.rs b/src-tauri/src/commands/provider_pool_cmd.rs new file mode 100644 index 000000000..b2bf6d267 --- /dev/null +++ b/src-tauri/src/commands/provider_pool_cmd.rs @@ -0,0 +1,635 @@ +//! Provider Pool Tauri 命令 + +use crate::database::DbConnection; +use crate::database::dao::provider_pool::ProviderPoolDao; +use crate::models::provider_pool_model::{ + AddCredentialRequest, CredentialData, CredentialDisplay, HealthCheckResult, OAuthStatus, + ProviderCredential, ProviderPoolOverview, UpdateCredentialRequest, +}; +use crate::services::provider_pool_service::ProviderPoolService; +use std::sync::Arc; +use tauri::State; +use std::fs; +use std::path::{Path, PathBuf}; +use uuid::Uuid; +use chrono::Utc; + +pub struct ProviderPoolServiceState(pub Arc); + +/// 展开路径中的 ~ 为用户主目录 +fn expand_tilde(path: &str) -> String { + if path.starts_with("~/") { + if let Some(home) = dirs::home_dir() { + return home.join(&path[2..]).to_string_lossy().to_string(); + } + } + path.to_string() +} + +/// 获取应用凭证存储目录 +fn get_credentials_dir() -> Result { + let app_data_dir = dirs::data_dir() + .ok_or_else(|| "无法获取应用数据目录".to_string())? + .join("proxycast") + .join("credentials"); + + // 确保目录存在 + if !app_data_dir.exists() { + fs::create_dir_all(&app_data_dir) + .map_err(|e| format!("创建凭证存储目录失败: {}", e))?; + } + + Ok(app_data_dir) +} + +/// 复制并重命名 OAuth 凭证文件 +fn copy_and_rename_credential_file( + source_path: &str, + provider_type: &str +) -> Result { + let expanded_source = expand_tilde(source_path); + let source = Path::new(&expanded_source); + + // 验证源文件存在 + if !source.exists() { + return Err(format!("凭证文件不存在: {}", expanded_source)); + } + + // 生成新的文件名:{provider_type}_{uuid}_{timestamp}.json + let uuid = Uuid::new_v4().to_string(); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let new_filename = format!("{}_{}_{}_{}.json", + provider_type, + &uuid[..8], // 使用 UUID 前8位 + timestamp, + provider_type + ); + + // 获取目标目录 + let credentials_dir = get_credentials_dir()?; + let target_path = credentials_dir.join(&new_filename); + + // 复制文件 + fs::copy(&source, &target_path) + .map_err(|e| format!("复制凭证文件失败: {}", e))?; + + // 返回新的文件路径 + Ok(target_path.to_string_lossy().to_string()) +} + +/// 删除凭证文件(如果在应用存储目录中) +fn cleanup_credential_file(file_path: &str) -> Result<(), String> { + let path = Path::new(file_path); + + // 只删除在应用凭证存储目录中的文件 + if let Ok(credentials_dir) = get_credentials_dir() { + if let Ok(canonical_path) = path.canonicalize() { + if let Ok(canonical_dir) = credentials_dir.canonicalize() { + if canonical_path.starts_with(canonical_dir) { + if let Err(e) = fs::remove_file(&canonical_path) { + // 只记录警告,不中断删除过程 + println!("Warning: Failed to delete credential file: {}", e); + } + } + } + } + } + + Ok(()) +} + +/// 获取凭证池概览 +#[tauri::command] +pub fn get_provider_pool_overview( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, +) -> Result, String> { + pool_service.0.get_overview(&db) +} + +/// 获取指定类型的凭证列表 +#[tauri::command] +pub fn get_provider_pool_credentials( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + provider_type: String, +) -> Result, String> { + pool_service.0.get_by_type(&db, &provider_type) +} + +/// 添加凭证 +#[tauri::command] +pub fn add_provider_pool_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + request: AddCredentialRequest, +) -> Result { + pool_service.0.add_credential( + &db, + &request.provider_type, + request.credential, + request.name, + request.check_health, + request.check_model_name, + ) +} + +/// 更新凭证 +#[tauri::command] +pub fn update_provider_pool_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + uuid: String, + request: UpdateCredentialRequest, +) -> Result { + // 如果需要重新上传文件,先处理文件上传 + if let Some(new_file_path) = request.new_creds_file_path { + // 获取当前凭证以确定类型 + let conn = db.lock().map_err(|e| e.to_string())?; + let current_credential = ProviderPoolDao::get_by_uuid(&conn, &uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("凭证不存在: {}", uuid))?; + + // 根据凭证类型复制新文件 + let new_stored_path = match ¤t_credential.credential { + CredentialData::KiroOAuth { creds_file_path } => { + // 清理旧文件 + cleanup_credential_file(creds_file_path)?; + copy_and_rename_credential_file(&new_file_path, "kiro")? + }, + CredentialData::GeminiOAuth { creds_file_path, .. } => { + // 清理旧文件 + cleanup_credential_file(creds_file_path)?; + copy_and_rename_credential_file(&new_file_path, "gemini")? + }, + CredentialData::QwenOAuth { creds_file_path } => { + // 清理旧文件 + cleanup_credential_file(creds_file_path)?; + copy_and_rename_credential_file(&new_file_path, "qwen")? + }, + _ => { + return Err("只有 OAuth 凭证支持重新上传文件".to_string()); + } + }; + + // 更新凭证数据 + let mut updated_cred = current_credential; + + // 更新凭证数据中的文件路径 + match &mut updated_cred.credential { + CredentialData::KiroOAuth { creds_file_path } => { + *creds_file_path = new_stored_path; + }, + CredentialData::GeminiOAuth { creds_file_path, project_id } => { + *creds_file_path = new_stored_path; + if let Some(new_pid) = request.new_project_id { + *project_id = Some(new_pid); + } + }, + CredentialData::QwenOAuth { creds_file_path } => { + *creds_file_path = new_stored_path; + }, + _ => {} + } + + // 应用其他更新 + if let Some(name) = request.name { + updated_cred.name = Some(name); + } + if let Some(is_disabled) = request.is_disabled { + updated_cred.is_disabled = is_disabled; + } + if let Some(check_health) = request.check_health { + updated_cred.check_health = check_health; + } + if let Some(check_model_name) = request.check_model_name { + updated_cred.check_model_name = Some(check_model_name); + } + if let Some(not_supported_models) = request.not_supported_models { + updated_cred.not_supported_models = not_supported_models; + } + + updated_cred.updated_at = Utc::now(); + + // 保存到数据库 + ProviderPoolDao::update(&conn, &updated_cred).map_err(|e| e.to_string())?; + + Ok(updated_cred) + } else { + // 常规更新,不涉及文件 + pool_service.0.update_credential( + &db, + &uuid, + request.name, + request.is_disabled, + request.check_health, + request.check_model_name, + request.not_supported_models, + ) + } +} + +/// 删除凭证 +#[tauri::command] +pub fn delete_provider_pool_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + uuid: String, +) -> Result { + pool_service.0.delete_credential(&db, &uuid) +} + +/// 切换凭证启用/禁用状态 +#[tauri::command] +pub fn toggle_provider_pool_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + uuid: String, + is_disabled: bool, +) -> Result { + pool_service.0.update_credential( + &db, + &uuid, + None, + Some(is_disabled), + None, + None, + None, + ) +} + +/// 重置凭证计数器 +#[tauri::command] +pub fn reset_provider_pool_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + uuid: String, +) -> Result<(), String> { + pool_service.0.reset_counters(&db, &uuid) +} + +/// 重置指定类型的所有凭证健康状态 +#[tauri::command] +pub fn reset_provider_pool_health( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + provider_type: String, +) -> Result { + pool_service.0.reset_health_by_type(&db, &provider_type) +} + +/// 执行单个凭证的健康检查 +#[tauri::command] +pub async fn check_provider_pool_credential_health( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + uuid: String, +) -> Result { + tracing::info!("[DEBUG] 开始健康检查 for uuid: {}", uuid); + let result = pool_service.0.check_credential_health(&db, &uuid).await; + match &result { + Ok(health) => tracing::info!("[DEBUG] 健康检查完成: success={}, message={:?}", health.success, health.message), + Err(err) => tracing::error!("[DEBUG] 健康检查失败: {}", err), + } + result +} + +/// 执行指定类型的所有凭证健康检查 +#[tauri::command] +pub async fn check_provider_pool_type_health( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + provider_type: String, +) -> Result, String> { + pool_service.0.check_type_health(&db, &provider_type).await +} + +/// 添加 Kiro OAuth 凭证(通过文件路径) +#[tauri::command] +pub fn add_kiro_oauth_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + creds_file_path: String, + name: Option, +) -> Result { + // 复制并重命名文件到应用存储目录 + let stored_file_path = copy_and_rename_credential_file(&creds_file_path, "kiro")?; + + pool_service.0.add_credential( + &db, + "kiro", + CredentialData::KiroOAuth { + creds_file_path: stored_file_path, + }, + name, + Some(true), + None, + ) +} + +/// 添加 Gemini OAuth 凭证(通过文件路径) +#[tauri::command] +pub fn add_gemini_oauth_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + creds_file_path: String, + project_id: Option, + name: Option, +) -> Result { + // 复制并重命名文件到应用存储目录 + let stored_file_path = copy_and_rename_credential_file(&creds_file_path, "gemini")?; + + pool_service.0.add_credential( + &db, + "gemini", + CredentialData::GeminiOAuth { + creds_file_path: stored_file_path, + project_id, + }, + name, + Some(true), + None, + ) +} + +/// 添加 Qwen OAuth 凭证(通过文件路径) +#[tauri::command] +pub fn add_qwen_oauth_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + creds_file_path: String, + name: Option, +) -> Result { + // 复制并重命名文件到应用存储目录 + let stored_file_path = copy_and_rename_credential_file(&creds_file_path, "qwen")?; + + pool_service.0.add_credential( + &db, + "qwen", + CredentialData::QwenOAuth { + creds_file_path: stored_file_path, + }, + name, + Some(true), + None, + ) +} + +/// 添加 OpenAI API Key 凭证 +#[tauri::command] +pub fn add_openai_key_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + api_key: String, + base_url: Option, + name: Option, +) -> Result { + pool_service.0.add_credential( + &db, + "openai", + CredentialData::OpenAIKey { api_key, base_url }, + name, + Some(true), + None, + ) +} + +/// 添加 Claude API Key 凭证 +#[tauri::command] +pub fn add_claude_key_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + api_key: String, + base_url: Option, + name: Option, +) -> Result { + pool_service.0.add_credential( + &db, + "claude", + CredentialData::ClaudeKey { api_key, base_url }, + name, + Some(true), + None, + ) +} + +/// 刷新凭证的 OAuth Token +#[tauri::command] +pub async fn refresh_pool_credential_token( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + uuid: String, +) -> Result { + tracing::info!("[DEBUG] 开始刷新 Token for uuid: {}", uuid); + let result = pool_service.0.refresh_credential_token(&db, &uuid).await; + match &result { + Ok(msg) => tracing::info!("[DEBUG] Token 刷新成功: {}", msg), + Err(err) => tracing::error!("[DEBUG] Token 刷新失败: {}", err), + } + result +} + +/// 获取凭证的 OAuth 状态 +#[tauri::command] +pub fn get_pool_credential_oauth_status( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + uuid: String, +) -> Result { + pool_service.0.get_credential_oauth_status(&db, &uuid) +} + +/// 调试 Kiro 凭证加载(从默认路径) +#[tauri::command] +pub async fn debug_kiro_credentials() -> Result { + use crate::providers::kiro::KiroProvider; + + let mut provider = KiroProvider::new(); + + let mut result = String::new(); + result.push_str("🔍 开始 Kiro 凭证调试 (默认路径)...\n\n"); + + match provider.load_credentials().await { + Ok(_) => { + result.push_str("✅ 凭证加载成功!\n"); + result.push_str(&format!("📄 认证方式: {:?}\n", provider.credentials.auth_method)); + result.push_str(&format!("🔑 有 client_id: {}\n", provider.credentials.client_id.is_some())); + result.push_str(&format!("🔒 有 client_secret: {}\n", provider.credentials.client_secret.is_some())); + result.push_str(&format!("🏷️ 有 clientIdHash: {}\n", provider.credentials.client_id_hash.is_some())); + + if let Some(hash) = &provider.credentials.client_id_hash { + result.push_str(&format!("🔗 clientIdHash: {}\n", hash)); + } + + let detected_method = provider.detect_auth_method(); + result.push_str(&format!("🎯 检测到的认证方式: {}\n", detected_method)); + + let refresh_url = provider.get_refresh_url(); + result.push_str(&format!("🌐 刷新端点: {}\n", refresh_url)); + + if let Some(client_id) = &provider.credentials.client_id { + result.push_str(&format!("🆔 client_id 前缀: {}...\n", &client_id[..std::cmp::min(20, client_id.len())])); + } + + result.push_str("\n🚀 尝试刷新 token...\n"); + match provider.refresh_token().await { + Ok(token) => { + result.push_str(&format!("✅ Token 刷新成功! Token 长度: {}\n", token.len())); + result.push_str(&format!("🎫 Token 前缀: {}...\n", &token[..std::cmp::min(50, token.len())])); + } + Err(e) => { + result.push_str(&format!("❌ Token 刷新失败: {}\n", e)); + } + } + } + Err(e) => { + result.push_str(&format!("❌ 凭证加载失败: {}\n", e)); + } + } + + Ok(result) +} + +/// 测试用户上传的凭证文件 +#[tauri::command] +pub async fn test_user_credentials() -> Result { + use crate::providers::kiro::KiroProvider; + use std::path::PathBuf; + + let mut result = String::new(); + result.push_str("🧪 测试用户上传的凭证文件...\n\n"); + + // 测试用户上传的凭证文件路径 + let user_creds_path = dirs::home_dir() + .ok_or("无法获取用户主目录".to_string())? + .join("Library/Application Support/proxycast/credentials/kiro_d8da9d58_1765757992_kiro.json"); + + result.push_str(&format!("📂 用户凭证路径: {}\n", user_creds_path.display())); + + // 检查文件是否存在 + if !user_creds_path.exists() { + result.push_str("❌ 用户凭证文件不存在!\n"); + result.push_str("💡 请确保文件路径正确,或重新上传凭证文件\n"); + return Ok(result); + } + + result.push_str("✅ 用户凭证文件存在\n\n"); + + // 读取并解析用户凭证文件 + match std::fs::read_to_string(&user_creds_path) { + Ok(content) => { + result.push_str("✅ 成功读取凭证文件\n"); + result.push_str(&format!("📄 文件大小: {} 字节\n", content.len())); + + // 尝试解析 JSON + match serde_json::from_str::(&content) { + Ok(json) => { + result.push_str("✅ JSON 格式有效\n"); + + // 检查关键字段 + let has_access_token = json.get("accessToken").and_then(|v| v.as_str()).is_some(); + let has_refresh_token = json.get("refreshToken").and_then(|v| v.as_str()).is_some(); + let auth_method = json.get("authMethod").and_then(|v| v.as_str()); + let client_id_hash = json.get("clientIdHash").and_then(|v| v.as_str()); + let region = json.get("region").and_then(|v| v.as_str()); + + result.push_str(&format!("🔑 有 accessToken: {}\n", has_access_token)); + result.push_str(&format!("🔄 有 refreshToken: {}\n", has_refresh_token)); + result.push_str(&format!("📄 authMethod: {:?}\n", auth_method)); + result.push_str(&format!("🏷️ clientIdHash: {:?}\n", client_id_hash)); + result.push_str(&format!("🌍 region: {:?}\n", region)); + + if let Some(hash) = client_id_hash { + // 检查 clientIdHash 对应的文件 + let hash_file_path = dirs::home_dir() + .unwrap() + .join(".aws/sso/cache") + .join(format!("{}.json", hash)); + + result.push_str(&format!("\n🔗 检查 clientIdHash 文件: {}\n", hash_file_path.display())); + + if hash_file_path.exists() { + result.push_str("✅ clientIdHash 文件存在\n"); + + match std::fs::read_to_string(&hash_file_path) { + Ok(hash_content) => { + match serde_json::from_str::(&hash_content) { + Ok(hash_json) => { + let has_client_id = hash_json.get("clientId").and_then(|v| v.as_str()).is_some(); + let has_client_secret = hash_json.get("clientSecret").and_then(|v| v.as_str()).is_some(); + + result.push_str(&format!("🆔 hash 文件有 clientId: {}\n", has_client_id)); + result.push_str(&format!("🔒 hash 文件有 clientSecret: {}\n", has_client_secret)); + + if has_client_id && has_client_secret { + result.push_str("✅ IdC 认证配置完整!\n"); + } else { + result.push_str("⚠️ IdC 认证配置不完整,将使用 social 认证\n"); + } + } + Err(e) => { + result.push_str(&format!("❌ 无法解析 hash 文件 JSON: {}\n", e)); + } + } + } + Err(e) => { + result.push_str(&format!("❌ 无法读取 hash 文件: {}\n", e)); + } + } + } else { + result.push_str("❌ clientIdHash 文件不存在\n"); + } + } + + // 现在使用我们的 KiroProvider 来测试加载 + result.push_str("\n🔧 使用 KiroProvider 测试加载...\n"); + + let mut provider = KiroProvider::new(); + // 设置凭证路径到用户文件 + provider.creds_path = Some(user_creds_path.clone()); + + match provider.load_credentials_from_path(&user_creds_path.to_string_lossy()).await { + Ok(_) => { + result.push_str("✅ KiroProvider 加载成功!\n"); + result.push_str(&format!("📄 最终认证方式: {:?}\n", provider.credentials.auth_method)); + result.push_str(&format!("🔑 最终有 client_id: {}\n", provider.credentials.client_id.is_some())); + result.push_str(&format!("🔒 最终有 client_secret: {}\n", provider.credentials.client_secret.is_some())); + + let detected_method = provider.detect_auth_method(); + result.push_str(&format!("🎯 检测到的认证方式: {}\n", detected_method)); + + let refresh_url = provider.get_refresh_url(); + result.push_str(&format!("🌐 刷新端点: {}\n", refresh_url)); + + result.push_str("\n🚀 尝试刷新 token...\n"); + match provider.refresh_token().await { + Ok(token) => { + result.push_str(&format!("✅ Token 刷新成功! Token 长度: {}\n", token.len())); + result.push_str(&format!("🎫 Token 前缀: {}...\n", &token[..std::cmp::min(50, token.len())])); + } + Err(e) => { + result.push_str(&format!("❌ Token 刷新失败: {}\n", e)); + } + } + } + Err(e) => { + result.push_str(&format!("❌ KiroProvider 加载失败: {}\n", e)); + } + } + } + Err(e) => { + result.push_str(&format!("❌ JSON 格式无效: {}\n", e)); + } + } + } + Err(e) => { + result.push_str(&format!("❌ 无法读取凭证文件: {}\n", e)); + } + } + + Ok(result) +} diff --git a/src-tauri/src/commands/route_cmd.rs b/src-tauri/src/commands/route_cmd.rs new file mode 100644 index 000000000..a4a72140e --- /dev/null +++ b/src-tauri/src/commands/route_cmd.rs @@ -0,0 +1,92 @@ +//! 路由相关 Tauri 命令 + +use crate::commands::provider_pool_cmd::ProviderPoolServiceState; +use crate::config; +use crate::database::DbConnection; +use crate::models::route_model::{RouteInfo, RouteListResponse}; + +/// 获取所有可用的路由端点 +#[tauri::command] +pub async fn get_available_routes( + db: tauri::State<'_, DbConnection>, + pool_service: tauri::State<'_, ProviderPoolServiceState>, +) -> Result { + // 获取配置中的服务器地址 + let config = config::load_config().unwrap_or_default(); + let base_url = format!("http://{}:{}", config.server.host, config.server.port); + + let routes = pool_service + .0 + .get_available_routes(db.inner(), &base_url) + .map_err(|e| e.to_string())?; + + // 添加默认路由 + let mut all_routes = vec![RouteInfo { + selector: "default".to_string(), + provider_type: "kiro".to_string(), + credential_count: 1, + endpoints: vec![ + crate::models::route_model::RouteEndpoint { + path: "/v1/messages".to_string(), + protocol: "claude".to_string(), + url: format!("{}/v1/messages", base_url), + }, + crate::models::route_model::RouteEndpoint { + path: "/v1/chat/completions".to_string(), + protocol: "openai".to_string(), + url: format!("{}/v1/chat/completions", base_url), + }, + ], + tags: vec!["默认".to_string()], + enabled: true, + }]; + all_routes.extend(routes); + + Ok(RouteListResponse { + base_url, + default_provider: "kiro".to_string(), + routes: all_routes, + }) +} + +/// 获取指定路由的 curl 示例 +#[tauri::command] +pub async fn get_route_curl_examples( + selector: String, + db: tauri::State<'_, DbConnection>, + pool_service: tauri::State<'_, ProviderPoolServiceState>, +) -> Result, String> { + let config = config::load_config().unwrap_or_default(); + let base_url = format!("http://{}:{}", config.server.host, config.server.port); + + let routes = pool_service + .0 + .get_available_routes(db.inner(), &base_url) + .map_err(|e| e.to_string())?; + + // 查找匹配的路由 + let route = routes + .iter() + .find(|r| r.selector == selector) + .or_else(|| { + // 如果是默认路由 + if selector == "default" { + None // 返回 None 让下面的代码生成默认示例 + } else { + None + } + }); + + let api_key = &config.server.api_key; + + match route { + Some(r) => Ok(r.generate_curl_examples(api_key)), + None => { + // 生成默认路由的示例 + let mut default_route = RouteInfo::new("default".to_string(), "kiro".to_string()); + default_route.add_endpoint(&base_url, "claude"); + default_route.add_endpoint(&base_url, "openai"); + Ok(default_route.generate_curl_examples(api_key)) + } + } +} diff --git a/src-tauri/src/database/dao/mod.rs b/src-tauri/src/database/dao/mod.rs index 2a93d1e1e..87198d2ff 100644 --- a/src-tauri/src/database/dao/mod.rs +++ b/src-tauri/src/database/dao/mod.rs @@ -1,4 +1,5 @@ pub mod mcp; pub mod prompts; +pub mod provider_pool; pub mod providers; pub mod skills; diff --git a/src-tauri/src/database/dao/provider_pool.rs b/src-tauri/src/database/dao/provider_pool.rs new file mode 100644 index 000000000..5b8fa9064 --- /dev/null +++ b/src-tauri/src/database/dao/provider_pool.rs @@ -0,0 +1,466 @@ +//! Provider Pool 数据访问对象 +//! +//! 提供凭证池的 CRUD 操作。 + +use crate::models::provider_pool_model::{ + CachedTokenInfo, CredentialData, PoolProviderType, ProviderCredential, ProviderPools, +}; +use chrono::{DateTime, TimeZone, Utc}; +use rusqlite::{params, Connection}; + +pub struct ProviderPoolDao; + +impl ProviderPoolDao { + /// 获取所有凭证 + pub fn get_all(conn: &Connection) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled, + check_health, check_model_name, not_supported_models, usage_count, error_count, + last_used, last_error_time, last_error_message, last_health_check_time, + last_health_check_model, created_at, updated_at + FROM provider_pool_credentials + ORDER BY provider_type, created_at ASC", + )?; + + let rows = stmt.query_map([], |row| Self::row_to_credential(row))?; + + let mut credentials = Vec::new(); + for row in rows { + if let Ok(cred) = row { + credentials.push(cred); + } + } + Ok(credentials) + } + + /// 获取指定类型的凭证 + pub fn get_by_type( + conn: &Connection, + provider_type: &PoolProviderType, + ) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled, + check_health, check_model_name, not_supported_models, usage_count, error_count, + last_used, last_error_time, last_error_message, last_health_check_time, + last_health_check_model, created_at, updated_at + FROM provider_pool_credentials + WHERE provider_type = ?1 + ORDER BY created_at ASC", + )?; + + let rows = stmt.query_map([provider_type.to_string()], |row| Self::row_to_credential(row))?; + + let mut credentials = Vec::new(); + for row in rows { + if let Ok(cred) = row { + credentials.push(cred); + } + } + Ok(credentials) + } + + /// 获取指定 UUID 的凭证 + pub fn get_by_uuid( + conn: &Connection, + uuid: &str, + ) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled, + check_health, check_model_name, not_supported_models, usage_count, error_count, + last_used, last_error_time, last_error_message, last_health_check_time, + last_health_check_model, created_at, updated_at + FROM provider_pool_credentials + WHERE uuid = ?1", + )?; + + let mut rows = stmt.query([uuid])?; + if let Some(row) = rows.next()? { + Ok(Some(Self::row_to_credential(row)?)) + } else { + Ok(None) + } + } + + /// 根据名称获取凭证 + pub fn get_by_name( + conn: &Connection, + name: &str, + ) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled, + check_health, check_model_name, not_supported_models, usage_count, error_count, + last_used, last_error_time, last_error_message, last_health_check_time, + last_health_check_model, created_at, updated_at + FROM provider_pool_credentials + WHERE name = ?1", + )?; + + let mut rows = stmt.query([name])?; + if let Some(row) = rows.next()? { + Ok(Some(Self::row_to_credential(row)?)) + } else { + Ok(None) + } + } + + /// 获取所有凭证按类型分组 + pub fn get_grouped(conn: &Connection) -> Result { + let all = Self::get_all(conn)?; + let mut grouped: ProviderPools = std::collections::HashMap::new(); + + for cred in all { + grouped + .entry(cred.provider_type) + .or_insert_with(Vec::new) + .push(cred); + } + + Ok(grouped) + } + + /// 插入新凭证 + pub fn insert(conn: &Connection, cred: &ProviderCredential) -> Result<(), rusqlite::Error> { + let credential_json = + serde_json::to_string(&cred.credential).unwrap_or_else(|_| "{}".to_string()); + let not_supported_models_json = + serde_json::to_string(&cred.not_supported_models).unwrap_or_else(|_| "[]".to_string()); + + conn.execute( + "INSERT INTO provider_pool_credentials + (uuid, provider_type, credential_data, name, is_healthy, is_disabled, + check_health, check_model_name, not_supported_models, usage_count, error_count, + last_used, last_error_time, last_error_message, last_health_check_time, + last_health_check_model, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", + params![ + cred.uuid, + cred.provider_type.to_string(), + credential_json, + cred.name, + cred.is_healthy, + cred.is_disabled, + cred.check_health, + cred.check_model_name, + not_supported_models_json, + cred.usage_count, + cred.error_count, + cred.last_used.map(|t| t.timestamp()), + cred.last_error_time.map(|t| t.timestamp()), + cred.last_error_message, + cred.last_health_check_time.map(|t| t.timestamp()), + cred.last_health_check_model, + cred.created_at.timestamp(), + cred.updated_at.timestamp(), + ], + )?; + Ok(()) + } + + /// 更新凭证 + pub fn update(conn: &Connection, cred: &ProviderCredential) -> Result<(), rusqlite::Error> { + let credential_json = + serde_json::to_string(&cred.credential).unwrap_or_else(|_| "{}".to_string()); + let not_supported_models_json = + serde_json::to_string(&cred.not_supported_models).unwrap_or_else(|_| "[]".to_string()); + + conn.execute( + "UPDATE provider_pool_credentials SET + provider_type = ?2, credential_data = ?3, name = ?4, is_healthy = ?5, + is_disabled = ?6, check_health = ?7, check_model_name = ?8, + not_supported_models = ?9, usage_count = ?10, error_count = ?11, + last_used = ?12, last_error_time = ?13, last_error_message = ?14, + last_health_check_time = ?15, last_health_check_model = ?16, updated_at = ?17 + WHERE uuid = ?1", + params![ + cred.uuid, + cred.provider_type.to_string(), + credential_json, + cred.name, + cred.is_healthy, + cred.is_disabled, + cred.check_health, + cred.check_model_name, + not_supported_models_json, + cred.usage_count, + cred.error_count, + cred.last_used.map(|t| t.timestamp()), + cred.last_error_time.map(|t| t.timestamp()), + cred.last_error_message, + cred.last_health_check_time.map(|t| t.timestamp()), + cred.last_health_check_model, + cred.updated_at.timestamp(), + ], + )?; + Ok(()) + } + + /// 删除凭证 + pub fn delete(conn: &Connection, uuid: &str) -> Result { + let affected = conn.execute( + "DELETE FROM provider_pool_credentials WHERE uuid = ?1", + [uuid], + )?; + Ok(affected > 0) + } + + /// 更新健康状态 + pub fn update_health_status( + conn: &Connection, + uuid: &str, + is_healthy: bool, + error_count: u32, + last_error_time: Option>, + last_error_message: Option<&str>, + last_health_check_time: Option>, + last_health_check_model: Option<&str>, + ) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE provider_pool_credentials SET + is_healthy = ?2, error_count = ?3, last_error_time = ?4, + last_error_message = ?5, last_health_check_time = ?6, + last_health_check_model = ?7, updated_at = ?8 + WHERE uuid = ?1", + params![ + uuid, + is_healthy, + error_count, + last_error_time.map(|t| t.timestamp()), + last_error_message, + last_health_check_time.map(|t| t.timestamp()), + last_health_check_model, + Utc::now().timestamp(), + ], + )?; + Ok(()) + } + + /// 更新使用统计 + pub fn update_usage( + conn: &Connection, + uuid: &str, + usage_count: u64, + last_used: DateTime, + ) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE provider_pool_credentials SET + usage_count = ?2, last_used = ?3, updated_at = ?4 + WHERE uuid = ?1", + params![uuid, usage_count, last_used.timestamp(), Utc::now().timestamp()], + )?; + Ok(()) + } + + /// 重置凭证计数器 + pub fn reset_counters(conn: &Connection, uuid: &str) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE provider_pool_credentials SET + usage_count = 0, error_count = 0, is_healthy = 1, + last_error_time = NULL, last_error_message = NULL, updated_at = ?2 + WHERE uuid = ?1", + params![uuid, Utc::now().timestamp()], + )?; + Ok(()) + } + + /// 重置指定类型的所有凭证健康状态 + pub fn reset_health_by_type( + conn: &Connection, + provider_type: &PoolProviderType, + ) -> Result { + let affected = conn.execute( + "UPDATE provider_pool_credentials SET + is_healthy = 1, error_count = 0, last_error_time = NULL, + last_error_message = NULL, updated_at = ?2 + WHERE provider_type = ?1", + params![provider_type.to_string(), Utc::now().timestamp()], + )?; + Ok(affected) + } + + /// 从数据库行转换为 ProviderCredential + fn row_to_credential(row: &rusqlite::Row) -> Result { + let uuid: String = row.get(0)?; + let provider_type_str: String = row.get(1)?; + let credential_json: String = row.get(2)?; + let name: Option = row.get(3)?; + let is_healthy: bool = row.get(4)?; + let is_disabled: bool = row.get(5)?; + let check_health: bool = row.get(6)?; + let check_model_name: Option = row.get(7)?; + let not_supported_models_json: Option = row.get(8)?; + let usage_count: u64 = row.get::<_, i64>(9)? as u64; + let error_count: u32 = row.get::<_, i32>(10)? as u32; + let last_used_ts: Option = row.get(11)?; + let last_error_time_ts: Option = row.get(12)?; + let last_error_message: Option = row.get(13)?; + let last_health_check_time_ts: Option = row.get(14)?; + let last_health_check_model: Option = row.get(15)?; + let created_at_ts: i64 = row.get(16)?; + let updated_at_ts: i64 = row.get(17)?; + + let provider_type: PoolProviderType = provider_type_str + .parse() + .unwrap_or(PoolProviderType::Kiro); + + let credential: CredentialData = serde_json::from_str(&credential_json).map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 2, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?; + + let not_supported_models: Vec = not_supported_models_json + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + + Ok(ProviderCredential { + uuid, + provider_type, + credential, + name, + is_healthy, + is_disabled, + check_health, + check_model_name, + not_supported_models, + usage_count, + error_count, + last_used: last_used_ts.and_then(|ts| Utc.timestamp_opt(ts, 0).single()), + last_error_time: last_error_time_ts.and_then(|ts| Utc.timestamp_opt(ts, 0).single()), + last_error_message, + last_health_check_time: last_health_check_time_ts + .and_then(|ts| Utc.timestamp_opt(ts, 0).single()), + last_health_check_model, + created_at: Utc.timestamp_opt(created_at_ts, 0).single().unwrap_or_default(), + updated_at: Utc.timestamp_opt(updated_at_ts, 0).single().unwrap_or_default(), + cached_token: None, // 从 get_token_cache 单独获取 + }) + } + + // ==================== Token 缓存操作 ==================== + + /// 获取凭证的 Token 缓存信息 + pub fn get_token_cache( + conn: &Connection, + uuid: &str, + ) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT cached_access_token, cached_refresh_token, token_expiry_time, + last_refresh_time, refresh_error_count, last_refresh_error + FROM provider_pool_credentials + WHERE uuid = ?1", + )?; + + let mut rows = stmt.query([uuid])?; + if let Some(row) = rows.next()? { + let access_token: Option = row.get(0)?; + let refresh_token: Option = row.get(1)?; + let expiry_time_str: Option = row.get(2)?; + let last_refresh_str: Option = row.get(3)?; + let refresh_error_count: i32 = row.get::<_, Option>(4)?.unwrap_or(0); + let last_refresh_error: Option = row.get(5)?; + + // 如果没有缓存的 token,返回 None + if access_token.is_none() { + return Ok(None); + } + + let expiry_time = expiry_time_str + .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) + .map(|dt| dt.with_timezone(&Utc)); + + let last_refresh = last_refresh_str + .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) + .map(|dt| dt.with_timezone(&Utc)); + + Ok(Some(CachedTokenInfo { + access_token, + refresh_token, + expiry_time, + last_refresh, + refresh_error_count: refresh_error_count as u32, + last_refresh_error, + })) + } else { + Ok(None) + } + } + + /// 更新凭证的 Token 缓存 + pub fn update_token_cache( + conn: &Connection, + uuid: &str, + token_info: &CachedTokenInfo, + ) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE provider_pool_credentials SET + cached_access_token = ?2, + cached_refresh_token = ?3, + token_expiry_time = ?4, + last_refresh_time = ?5, + refresh_error_count = ?6, + last_refresh_error = ?7, + updated_at = ?8 + WHERE uuid = ?1", + params![ + uuid, + token_info.access_token, + token_info.refresh_token, + token_info.expiry_time.map(|t| t.to_rfc3339()), + token_info.last_refresh.map(|t| t.to_rfc3339()), + token_info.refresh_error_count as i32, + token_info.last_refresh_error, + Utc::now().timestamp(), + ], + )?; + Ok(()) + } + + /// 清除凭证的 Token 缓存 + pub fn clear_token_cache(conn: &Connection, uuid: &str) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE provider_pool_credentials SET + cached_access_token = NULL, + cached_refresh_token = NULL, + token_expiry_time = NULL, + last_refresh_time = NULL, + refresh_error_count = 0, + last_refresh_error = NULL, + updated_at = ?2 + WHERE uuid = ?1", + params![uuid, Utc::now().timestamp()], + )?; + Ok(()) + } + + /// 记录 Token 刷新错误 + pub fn record_token_refresh_error( + conn: &Connection, + uuid: &str, + error_message: &str, + ) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE provider_pool_credentials SET + refresh_error_count = COALESCE(refresh_error_count, 0) + 1, + last_refresh_error = ?2, + updated_at = ?3 + WHERE uuid = ?1", + params![uuid, error_message, Utc::now().timestamp()], + )?; + Ok(()) + } + + /// 重置 Token 刷新错误计数 + pub fn reset_token_refresh_errors(conn: &Connection, uuid: &str) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE provider_pool_credentials SET + refresh_error_count = 0, + last_refresh_error = NULL, + updated_at = ?2 + WHERE uuid = ?1", + params![uuid, Utc::now().timestamp()], + )?; + Ok(()) + } +} diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 3e58aaacb..4820ec4ff 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -94,5 +94,62 @@ pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> { [], )?; + // Provider Pool 凭证表 + conn.execute( + "CREATE TABLE IF NOT EXISTS provider_pool_credentials ( + uuid TEXT PRIMARY KEY, + provider_type TEXT NOT NULL, + credential_data TEXT NOT NULL, + name TEXT, + is_healthy INTEGER DEFAULT 1, + is_disabled INTEGER DEFAULT 0, + check_health INTEGER DEFAULT 1, + check_model_name TEXT, + not_supported_models TEXT, + usage_count INTEGER DEFAULT 0, + error_count INTEGER DEFAULT 0, + last_used INTEGER, + last_error_time INTEGER, + last_error_message TEXT, + last_health_check_time INTEGER, + last_health_check_model TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + [], + )?; + + // 创建 provider_type 索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_provider_pool_type ON provider_pool_credentials(provider_type)", + [], + )?; + + // Migration: 添加 Token 缓存字段 + let _ = conn.execute( + "ALTER TABLE provider_pool_credentials ADD COLUMN cached_access_token TEXT", + [], + ); + let _ = conn.execute( + "ALTER TABLE provider_pool_credentials ADD COLUMN cached_refresh_token TEXT", + [], + ); + let _ = conn.execute( + "ALTER TABLE provider_pool_credentials ADD COLUMN token_expiry_time TEXT", + [], + ); + let _ = conn.execute( + "ALTER TABLE provider_pool_credentials ADD COLUMN last_refresh_time TEXT", + [], + ); + let _ = conn.execute( + "ALTER TABLE provider_pool_credentials ADD COLUMN refresh_error_count INTEGER DEFAULT 0", + [], + ); + let _ = conn.execute( + "ALTER TABLE provider_pool_credentials ADD COLUMN last_refresh_error TEXT", + [], + ); + Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2b879ebb1..1feb33255 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,8 +12,14 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; +use commands::provider_pool_cmd::ProviderPoolServiceState; use commands::skill_cmd::SkillServiceState; +use services::provider_pool_service::ProviderPoolService; use services::skill_service::SkillService; +use services::token_cache_service::TokenCacheService; + +/// TokenCacheService 状态封装 +pub struct TokenCacheServiceState(pub Arc); #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -125,12 +131,20 @@ pub type LogState = Arc>; async fn start_server( state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>, + db: tauri::State<'_, database::DbConnection>, + pool_service: tauri::State<'_, ProviderPoolServiceState>, + token_cache: tauri::State<'_, TokenCacheServiceState>, ) -> Result { let mut s = state.write().await; logs.write().await.add("info", "Starting server..."); - s.start(logs.inner().clone()) - .await - .map_err(|e| e.to_string())?; + s.start( + logs.inner().clone(), + pool_service.0.clone(), + token_cache.0.clone(), + Some(db.inner().clone()), + ) + .await + .map_err(|e| e.to_string())?; logs.write().await.add( "info", &format!( @@ -1269,6 +1283,14 @@ pub fn run() { let skill_service = SkillService::new().expect("Failed to initialize SkillService"); let skill_service_state = SkillServiceState(Arc::new(skill_service)); + // Initialize ProviderPoolService + let provider_pool_service = ProviderPoolService::new(); + let provider_pool_service_state = ProviderPoolServiceState(Arc::new(provider_pool_service)); + + // Initialize TokenCacheService + let token_cache_service = TokenCacheService::new(); + let token_cache_service_state = TokenCacheServiceState(Arc::new(token_cache_service)); + // Initialize default skill repos { let conn = db.lock().expect("Failed to lock database"); @@ -1279,9 +1301,13 @@ pub fn run() { // Clone for setup hook let state_clone = state.clone(); let logs_clone = logs.clone(); + let db_clone = db.clone(); + let pool_service_clone = provider_pool_service_state.0.clone(); + let token_cache_clone = token_cache_service_state.0.clone(); tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec!["--minimized"]), @@ -1290,10 +1316,15 @@ pub fn run() { .manage(logs) .manage(db) .manage(skill_service_state) + .manage(provider_pool_service_state) + .manage(token_cache_service_state) .setup(move |_app| { // 自动启动服务器 let state = state_clone.clone(); let logs = logs_clone.clone(); + let db = db_clone.clone(); + let pool_service = pool_service_clone.clone(); + let token_cache = token_cache_clone.clone(); tauri::async_runtime::spawn(async move { // 先加载凭证 { @@ -1312,7 +1343,7 @@ pub fn run() { logs.write() .await .add("info", "[启动] 正在自动启动服务器..."); - match s.start(logs.clone()).await { + match s.start(logs.clone(), pool_service, token_cache, Some(db)).await { Ok(_) => { let host = s.config.server.host.clone(); let port = s.config.server.port; @@ -1425,6 +1456,29 @@ pub fn run() { commands::skill_cmd::get_skill_repos, commands::skill_cmd::add_skill_repo, commands::skill_cmd::remove_skill_repo, + // Provider Pool commands + commands::provider_pool_cmd::get_provider_pool_overview, + commands::provider_pool_cmd::get_provider_pool_credentials, + commands::provider_pool_cmd::add_provider_pool_credential, + commands::provider_pool_cmd::update_provider_pool_credential, + commands::provider_pool_cmd::delete_provider_pool_credential, + commands::provider_pool_cmd::toggle_provider_pool_credential, + commands::provider_pool_cmd::reset_provider_pool_credential, + commands::provider_pool_cmd::reset_provider_pool_health, + commands::provider_pool_cmd::check_provider_pool_credential_health, + commands::provider_pool_cmd::check_provider_pool_type_health, + commands::provider_pool_cmd::add_kiro_oauth_credential, + commands::provider_pool_cmd::add_gemini_oauth_credential, + commands::provider_pool_cmd::add_qwen_oauth_credential, + commands::provider_pool_cmd::add_openai_key_credential, + commands::provider_pool_cmd::add_claude_key_credential, + commands::provider_pool_cmd::refresh_pool_credential_token, + commands::provider_pool_cmd::get_pool_credential_oauth_status, + commands::provider_pool_cmd::debug_kiro_credentials, + commands::provider_pool_cmd::test_user_credentials, + // Route commands + commands::route_cmd::get_available_routes, + commands::route_cmd::get_route_curl_examples, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index f8e78341a..4264ac3e0 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -5,6 +5,8 @@ pub mod mcp_model; pub mod openai; pub mod prompt_model; pub mod provider_model; +pub mod provider_pool_model; +pub mod route_model; pub mod skill_model; #[allow(unused_imports)] @@ -17,4 +19,5 @@ pub use mcp_model::McpServer; pub use openai::*; pub use prompt_model::Prompt; pub use provider_model::Provider; +pub use provider_pool_model::*; pub use skill_model::{Skill, SkillMetadata, SkillRepo, SkillState, SkillStates}; diff --git a/src-tauri/src/models/provider_pool_model.rs b/src-tauri/src/models/provider_pool_model.rs new file mode 100644 index 000000000..37be2fcbb --- /dev/null +++ b/src-tauri/src/models/provider_pool_model.rs @@ -0,0 +1,501 @@ +//! Provider Pool 数据模型 +//! +//! 支持多凭证池管理,包括健康检测、负载均衡、故障转移等功能。 + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use uuid::Uuid; + +/// Provider 类型枚举 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PoolProviderType { + Kiro, + Gemini, + Qwen, + #[serde(rename = "openai")] + OpenAI, + Claude, +} + +impl std::fmt::Display for PoolProviderType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PoolProviderType::Kiro => write!(f, "kiro"), + PoolProviderType::Gemini => write!(f, "gemini"), + PoolProviderType::Qwen => write!(f, "qwen"), + PoolProviderType::OpenAI => write!(f, "openai"), + PoolProviderType::Claude => write!(f, "claude"), + } + } +} + +impl std::str::FromStr for PoolProviderType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "kiro" => Ok(PoolProviderType::Kiro), + "gemini" => Ok(PoolProviderType::Gemini), + "qwen" => Ok(PoolProviderType::Qwen), + "openai" => Ok(PoolProviderType::OpenAI), + "claude" => Ok(PoolProviderType::Claude), + _ => Err(format!("Invalid provider type: {s}")), + } + } +} + +/// 凭证数据,根据 Provider 类型不同而不同 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum CredentialData { + /// Kiro OAuth 凭证(文件路径) + KiroOAuth { + creds_file_path: String, + }, + /// Gemini OAuth 凭证(文件路径) + GeminiOAuth { + creds_file_path: String, + project_id: Option, + }, + /// Qwen OAuth 凭证(文件路径) + QwenOAuth { + creds_file_path: String, + }, + /// OpenAI API Key 凭证 + OpenAIKey { + api_key: String, + base_url: Option, + }, + /// Claude API Key 凭证 + ClaudeKey { + api_key: String, + base_url: Option, + }, +} + +impl CredentialData { + /// 获取凭证的显示名称(隐藏敏感信息) + pub fn display_name(&self) -> String { + match self { + CredentialData::KiroOAuth { creds_file_path } => { + format!("Kiro OAuth: {}", mask_path(creds_file_path)) + } + CredentialData::GeminiOAuth { + creds_file_path, .. + } => { + format!("Gemini OAuth: {}", mask_path(creds_file_path)) + } + CredentialData::QwenOAuth { creds_file_path } => { + format!("Qwen OAuth: {}", mask_path(creds_file_path)) + } + CredentialData::OpenAIKey { api_key, .. } => { + format!("OpenAI: {}", mask_key(api_key)) + } + CredentialData::ClaudeKey { api_key, .. } => { + format!("Claude: {}", mask_key(api_key)) + } + } + } + + /// 获取 Provider 类型 + pub fn provider_type(&self) -> PoolProviderType { + match self { + CredentialData::KiroOAuth { .. } => PoolProviderType::Kiro, + CredentialData::GeminiOAuth { .. } => PoolProviderType::Gemini, + CredentialData::QwenOAuth { .. } => PoolProviderType::Qwen, + CredentialData::OpenAIKey { .. } => PoolProviderType::OpenAI, + CredentialData::ClaudeKey { .. } => PoolProviderType::Claude, + } + } +} + +/// 单个凭证 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderCredential { + /// 唯一标识符 + pub uuid: String, + /// Provider 类型 + pub provider_type: PoolProviderType, + /// 凭证数据 + pub credential: CredentialData, + /// 备注/名称 + pub name: Option, + /// 是否健康 + #[serde(default = "default_true")] + pub is_healthy: bool, + /// 是否禁用(手动禁用) + #[serde(default)] + pub is_disabled: bool, + /// 是否启用自动健康检查 + #[serde(default = "default_true")] + pub check_health: bool, + /// 自定义健康检查模型 + pub check_model_name: Option, + /// 不支持的模型列表(黑名单) + #[serde(default)] + pub not_supported_models: Vec, + /// 使用次数 + #[serde(default)] + pub usage_count: u64, + /// 错误次数 + #[serde(default)] + pub error_count: u32, + /// 最后使用时间 + pub last_used: Option>, + /// 最后错误时间 + pub last_error_time: Option>, + /// 最后错误消息 + pub last_error_message: Option, + /// 最后健康检查时间 + pub last_health_check_time: Option>, + /// 最后健康检查使用的模型 + pub last_health_check_model: Option, + /// 创建时间 + pub created_at: DateTime, + /// 更新时间 + pub updated_at: DateTime, + /// Token 缓存信息 + #[serde(default)] + pub cached_token: Option, +} + +fn default_true() -> bool { + true +} + +impl ProviderCredential { + /// 创建新凭证 + pub fn new(provider_type: PoolProviderType, credential: CredentialData) -> Self { + let now = Utc::now(); + Self { + uuid: Uuid::new_v4().to_string(), + provider_type, + credential, + name: None, + is_healthy: true, + is_disabled: false, + check_health: true, + check_model_name: None, + not_supported_models: Vec::new(), + usage_count: 0, + error_count: 0, + last_used: None, + last_error_time: None, + last_error_message: None, + last_health_check_time: None, + last_health_check_model: None, + created_at: now, + updated_at: now, + cached_token: None, + } + } + + /// 是否可用(健康且未禁用) + pub fn is_available(&self) -> bool { + self.is_healthy && !self.is_disabled + } + + /// 是否支持指定模型 + pub fn supports_model(&self, model: &str) -> bool { + !self.not_supported_models.contains(&model.to_string()) + } + + /// 标记为健康 + pub fn mark_healthy(&mut self, check_model: Option) { + self.is_healthy = true; + self.error_count = 0; + self.last_health_check_time = Some(Utc::now()); + self.last_health_check_model = check_model; + self.updated_at = Utc::now(); + } + + /// 标记为不健康 + pub fn mark_unhealthy(&mut self, error_message: Option) { + self.error_count += 1; + self.last_error_time = Some(Utc::now()); + self.last_error_message = error_message; + self.updated_at = Utc::now(); + // 错误次数达到阈值则标记为不健康 + if self.error_count >= 3 { + self.is_healthy = false; + } + } + + /// 记录使用 + pub fn record_usage(&mut self) { + self.usage_count += 1; + self.last_used = Some(Utc::now()); + self.updated_at = Utc::now(); + } + + /// 重置计数器 + pub fn reset_counters(&mut self) { + self.usage_count = 0; + self.error_count = 0; + self.is_healthy = true; + self.last_error_time = None; + self.last_error_message = None; + self.updated_at = Utc::now(); + } +} + +/// 凭证池统计信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PoolStats { + /// 总凭证数 + pub total_count: usize, + /// 健康凭证数 + pub healthy_count: usize, + /// 禁用凭证数 + pub disabled_count: usize, + /// 总使用次数 + pub total_usage: u64, + /// 总错误次数 + pub total_errors: u64, + /// 最后更新时间 + pub last_update: DateTime, +} + +impl PoolStats { + pub fn from_credentials(credentials: &[ProviderCredential]) -> Self { + Self { + total_count: credentials.len(), + healthy_count: credentials.iter().filter(|c| c.is_healthy).count(), + disabled_count: credentials.iter().filter(|c| c.is_disabled).count(), + total_usage: credentials.iter().map(|c| c.usage_count).sum(), + total_errors: credentials.iter().map(|c| c.error_count as u64).sum(), + last_update: Utc::now(), + } + } +} + +/// 健康检查结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealthCheckResult { + pub uuid: String, + pub success: bool, + pub model: Option, + pub message: Option, + pub duration_ms: u64, +} + +/// OAuth 凭证状态 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuthStatus { + /// 是否有 access_token + pub has_access_token: bool, + /// 是否有 refresh_token + pub has_refresh_token: bool, + /// token 是否有效 + pub is_token_valid: bool, + /// 过期信息 + pub expiry_info: Option, + /// 凭证文件路径 + pub creds_path: String, +} + +/// Token 缓存状态(用于前端展示) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TokenCacheStatus { + /// 是否有缓存的 token + pub has_cached_token: bool, + /// Token 是否有效 + pub is_valid: bool, + /// Token 是否即将过期(5分钟内) + pub is_expiring_soon: bool, + /// 过期时间 + pub expiry_time: Option, + /// 最后刷新时间 + pub last_refresh: Option, + /// 连续刷新失败次数 + pub refresh_error_count: u32, + /// 最后刷新错误信息 + pub last_refresh_error: Option, +} + +/// Token 缓存信息 +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CachedTokenInfo { + /// 缓存的 access_token + pub access_token: Option, + /// 缓存的 refresh_token(刷新后可能变化) + pub refresh_token: Option, + /// Token 过期时间 + pub expiry_time: Option>, + /// 最后刷新时间 + pub last_refresh: Option>, + /// 连续刷新失败次数 + #[serde(default)] + pub refresh_error_count: u32, + /// 最后刷新错误信息 + pub last_refresh_error: Option, +} + +impl CachedTokenInfo { + /// 检查 token 是否有效(存在且未过期) + pub fn is_valid(&self) -> bool { + if self.access_token.is_none() { + return false; + } + match &self.expiry_time { + Some(expiry) => *expiry > Utc::now(), + None => true, // 没有过期时间,假设有效 + } + } + + /// 检查 token 是否即将过期(5分钟内) + pub fn is_expiring_soon(&self) -> bool { + match &self.expiry_time { + Some(expiry) => { + let threshold = Utc::now() + chrono::Duration::minutes(5); + *expiry <= threshold + } + None => false, // 没有过期时间,假设不会过期 + } + } + + /// 检查 token 是否需要刷新(无效或即将过期) + pub fn needs_refresh(&self) -> bool { + !self.is_valid() || self.is_expiring_soon() + } +} + +/// 默认健康检查模型 +pub fn get_default_check_model(provider_type: PoolProviderType) -> &'static str { + match provider_type { + PoolProviderType::Kiro => "claude-haiku-4-5", + PoolProviderType::Gemini => "gemini-2.5-flash", + PoolProviderType::Qwen => "qwen3-coder-flash", + PoolProviderType::OpenAI => "gpt-3.5-turbo", + PoolProviderType::Claude => "claude-3-5-haiku-latest", + } +} + +/// 凭证池前端展示数据 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CredentialDisplay { + pub uuid: String, + pub provider_type: String, + pub credential_type: String, + pub name: Option, + pub display_credential: String, + pub is_healthy: bool, + pub is_disabled: bool, + pub usage_count: u64, + pub error_count: u32, + pub last_used: Option, + pub last_error_message: Option, + pub last_health_check_time: Option, + pub last_health_check_model: Option, + pub oauth_status: Option, + pub token_cache_status: Option, +} + +/// 获取凭证类型字符串 +fn get_credential_type(cred: &CredentialData) -> String { + match cred { + CredentialData::KiroOAuth { .. } => "kiro_oauth".to_string(), + CredentialData::GeminiOAuth { .. } => "gemini_oauth".to_string(), + CredentialData::QwenOAuth { .. } => "qwen_oauth".to_string(), + CredentialData::OpenAIKey { .. } => "openai_key".to_string(), + CredentialData::ClaudeKey { .. } => "claude_key".to_string(), + } +} + +/// 获取 OAuth 凭证的文件路径 +pub fn get_oauth_creds_path(cred: &CredentialData) -> Option { + match cred { + CredentialData::KiroOAuth { creds_file_path } => Some(creds_file_path.clone()), + CredentialData::GeminiOAuth { creds_file_path, .. } => Some(creds_file_path.clone()), + CredentialData::QwenOAuth { creds_file_path } => Some(creds_file_path.clone()), + _ => None, + } +} + +impl From<&ProviderCredential> for CredentialDisplay { + fn from(cred: &ProviderCredential) -> Self { + // 构建 token 缓存状态 + let token_cache_status = cred.cached_token.as_ref().map(|cache| TokenCacheStatus { + has_cached_token: cache.access_token.is_some(), + is_valid: cache.is_valid(), + is_expiring_soon: cache.is_expiring_soon(), + expiry_time: cache.expiry_time.map(|t| t.to_rfc3339()), + last_refresh: cache.last_refresh.map(|t| t.to_rfc3339()), + refresh_error_count: cache.refresh_error_count, + last_refresh_error: cache.last_refresh_error.clone(), + }); + + Self { + uuid: cred.uuid.clone(), + provider_type: cred.provider_type.to_string(), + credential_type: get_credential_type(&cred.credential), + name: cred.name.clone(), + display_credential: cred.credential.display_name(), + is_healthy: cred.is_healthy, + is_disabled: cred.is_disabled, + usage_count: cred.usage_count, + error_count: cred.error_count, + last_used: cred.last_used.map(|t| t.to_rfc3339()), + last_error_message: cred.last_error_message.clone(), + last_health_check_time: cred.last_health_check_time.map(|t| t.to_rfc3339()), + last_health_check_model: cred.last_health_check_model.clone(), + oauth_status: None, // 需要单独调用获取 + token_cache_status, + } + } +} + +/// Provider 池概览(按类型分组的统计) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderPoolOverview { + pub provider_type: String, + pub stats: PoolStats, + pub credentials: Vec, +} + +// 辅助函数:隐藏路径中的用户名 +fn mask_path(path: &str) -> String { + if let Some(home) = dirs::home_dir() { + let home_str = home.to_string_lossy(); + path.replace(&*home_str, "~") + } else { + path.to_string() + } +} + +// 辅助函数:隐藏 API Key +fn mask_key(key: &str) -> String { + if key.len() <= 12 { + "****".to_string() + } else { + format!("{}...{}", &key[..6], &key[key.len() - 4..]) + } +} + +/// 添加凭证的请求结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddCredentialRequest { + pub provider_type: String, + pub credential: CredentialData, + pub name: Option, + pub check_health: Option, + pub check_model_name: Option, +} + +/// 更新凭证的请求结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateCredentialRequest { + pub name: Option, + pub is_disabled: Option, + pub check_health: Option, + pub check_model_name: Option, + pub not_supported_models: Option>, + /// 新的凭证文件路径(仅适用于OAuth凭证,用于重新上传文件) + pub new_creds_file_path: Option, + /// OAuth相关:新的project_id(仅适用于Gemini) + pub new_project_id: Option, +} + +pub type ProviderPools = HashMap>; diff --git a/src-tauri/src/models/route_model.rs b/src-tauri/src/models/route_model.rs new file mode 100644 index 000000000..f6363105b --- /dev/null +++ b/src-tauri/src/models/route_model.rs @@ -0,0 +1,135 @@ +//! 路由模型 +//! +//! 用于多供应商路由功能的数据结构定义。 + +use serde::{Deserialize, Serialize}; + +/// 单个路由信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RouteInfo { + /// 路由选择器 (provider 类型或凭证名称) + pub selector: String, + /// Provider 类型 + pub provider_type: String, + /// 关联的凭证数量 + pub credential_count: usize, + /// 可用的端点列表 + pub endpoints: Vec, + /// 标签 (如 "突破限制", "官方API/三方") + pub tags: Vec, + /// 是否启用 + pub enabled: bool, +} + +/// 路由端点 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RouteEndpoint { + /// 端点路径 + pub path: String, + /// 协议类型 + pub protocol: String, // "openai" 或 "claude" + /// 完整 URL + pub url: String, +} + +/// 路由列表响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RouteListResponse { + /// 服务器基础 URL + pub base_url: String, + /// 默认 Provider + pub default_provider: String, + /// 所有可用路由 + pub routes: Vec, +} + +/// curl 示例 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CurlExample { + /// 描述 + pub description: String, + /// curl 命令 + pub command: String, +} + +impl RouteInfo { + /// 创建新的路由信息 + pub fn new(selector: String, provider_type: String) -> Self { + Self { + selector, + provider_type, + credential_count: 0, + endpoints: Vec::new(), + tags: Vec::new(), + enabled: true, + } + } + + /// 添加端点 + pub fn add_endpoint(&mut self, base_url: &str, protocol: &str) { + let path = match protocol { + "claude" => format!("/{}/v1/messages", self.selector), + "openai" => format!("/{}/v1/chat/completions", self.selector), + _ => return, + }; + let url = format!("{}{}", base_url, path); + self.endpoints.push(RouteEndpoint { + path, + protocol: protocol.to_string(), + url, + }); + } + + /// 生成 curl 示例 + pub fn generate_curl_examples(&self, api_key: &str) -> Vec { + let mut examples = Vec::new(); + + for endpoint in &self.endpoints { + let (model, body) = match endpoint.protocol.as_str() { + "claude" => { + let model = match self.provider_type.as_str() { + "kiro" | "claude" => "claude-sonnet-4-5", + "gemini" => "gemini-2.5-flash", + "qwen" => "qwen3-coder-plus", + "openai" => "gpt-4", + _ => "claude-sonnet-4-5", + }; + (model, format!(r#"{{ + "model": "{}", + "max_tokens": 1024, + "messages": [{{"role": "user", "content": "Hello!"}}] +}}"#, model)) + } + "openai" => { + let model = match self.provider_type.as_str() { + "kiro" | "claude" => "claude-sonnet-4-5", + "gemini" => "gemini-2.5-flash", + "qwen" => "qwen3-coder-plus", + "openai" => "gpt-4", + _ => "claude-sonnet-4-5", + }; + (model, format!(r#"{{ + "model": "{}", + "messages": [{{"role": "user", "content": "Hello!"}}] +}}"#, model)) + } + _ => continue, + }; + + let command = format!( + r#"curl {} \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {}" \ + -d '{}'"#, + endpoint.url, api_key, body + ); + + examples.push(CurlExample { + description: format!("{} 协议", endpoint.protocol.to_uppercase()), + command, + }); + } + + examples + } +} diff --git a/src-tauri/src/providers/claude_custom.rs b/src-tauri/src/providers/claude_custom.rs index 6c2e602b6..e7a84dca5 100644 --- a/src-tauri/src/providers/claude_custom.rs +++ b/src-tauri/src/providers/claude_custom.rs @@ -1,4 +1,6 @@ //! Claude Custom Provider (自定义 Claude API) +use crate::models::anthropic::AnthropicMessagesRequest; +use crate::models::openai::{ChatCompletionRequest, ContentPart, MessageContent}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; @@ -29,6 +31,18 @@ impl ClaudeCustomProvider { Self::default() } + /// 使用 API key 和 base_url 创建 Provider + pub fn with_config(api_key: String, base_url: Option) -> Self { + Self { + config: ClaudeCustomConfig { + api_key: Some(api_key), + base_url, + enabled: true, + }, + client: Client::new(), + } + } + pub fn get_base_url(&self) -> String { self.config .base_url @@ -40,6 +54,148 @@ impl ClaudeCustomProvider { self.config.api_key.is_some() && self.config.enabled } + /// 调用 Anthropic API(原生格式) + pub async fn call_api( + &self, + request: &AnthropicMessagesRequest, + ) -> Result> { + let api_key = self + .config + .api_key + .as_ref() + .ok_or("Claude API key not configured")?; + + let base_url = self.get_base_url(); + let url = format!("{base_url}/v1/messages"); + + let resp = self + .client + .post(&url) + .header("x-api-key", api_key) + .header("anthropic-version", "2023-06-01") + .header("Content-Type", "application/json") + .json(request) + .send() + .await?; + + Ok(resp) + } + + /// 调用 OpenAI 格式的 API(内部转换为 Anthropic 格式) + pub async fn call_openai_api( + &self, + request: &ChatCompletionRequest, + ) -> Result> { + // 手动转换 OpenAI 请求为 Anthropic 格式 + let mut anthropic_messages = Vec::new(); + let mut system_content = None; + + for msg in &request.messages { + let role = &msg.role; + + // 提取消息内容 + let content = match &msg.content { + Some(MessageContent::Text(text)) => text.clone(), + Some(MessageContent::Parts(parts)) => { + // 合并所有文本部分 + parts + .iter() + .filter_map(|p| { + if let ContentPart::Text { text } = p { + Some(text.clone()) + } else { + None + } + }) + .collect::>() + .join("") + } + None => String::new(), + }; + + if role == "system" { + system_content = Some(content); + } else { + let anthropic_role = if role == "assistant" { + "assistant" + } else { + "user" + }; + anthropic_messages.push(serde_json::json!({ + "role": anthropic_role, + "content": content + })); + } + } + + let mut anthropic_body = serde_json::json!({ + "model": request.model, + "max_tokens": request.max_tokens.unwrap_or(4096), + "messages": anthropic_messages + }); + + if let Some(sys) = system_content { + anthropic_body["system"] = serde_json::json!(sys); + } + + let api_key = self + .config + .api_key + .as_ref() + .ok_or("Claude API key not configured")?; + + let base_url = self.get_base_url(); + let url = format!("{base_url}/v1/messages"); + + let resp = self + .client + .post(&url) + .header("x-api-key", api_key) + .header("anthropic-version", "2023-06-01") + .header("Content-Type", "application/json") + .json(&anthropic_body) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(format!("Claude API error: {status} - {body}").into()); + } + + let anthropic_resp: serde_json::Value = resp.json().await?; + + // 转换回 OpenAI 格式 + let content = anthropic_resp["content"] + .as_array() + .and_then(|arr| arr.first()) + .and_then(|block| block["text"].as_str()) + .unwrap_or(""); + + Ok(serde_json::json!({ + "id": format!("chatcmpl-{}", uuid::Uuid::new_v4()), + "object": "chat.completion", + "created": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + "model": request.model, + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": content + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": anthropic_resp["usage"]["input_tokens"].as_u64().unwrap_or(0), + "completion_tokens": anthropic_resp["usage"]["output_tokens"].as_u64().unwrap_or(0), + "total_tokens": 0 + } + })) + } + pub async fn messages( &self, request: &serde_json::Value, diff --git a/src-tauri/src/providers/gemini.rs b/src-tauri/src/providers/gemini.rs index 947a8dfe0..e86c0cdd6 100644 --- a/src-tauri/src/providers/gemini.rs +++ b/src-tauri/src/providers/gemini.rs @@ -157,6 +157,16 @@ impl GeminiProvider { Ok(()) } + pub async fn load_credentials_from_path( + &mut self, + path: &str, + ) -> Result<(), Box> { + let content = tokio::fs::read_to_string(path).await?; + let creds: GeminiCredentials = serde_json::from_str(&content)?; + self.credentials = creds; + Ok(()) + } + pub async fn save_credentials(&self) -> Result<(), Box> { let path = Self::default_creds_path(); if let Some(parent) = path.parent() { diff --git a/src-tauri/src/providers/kiro.rs b/src-tauri/src/providers/kiro.rs index 824b802ad..b6f7e44e3 100644 --- a/src-tauri/src/providers/kiro.rs +++ b/src-tauri/src/providers/kiro.rs @@ -47,6 +47,7 @@ pub struct KiroCredentials { pub expires_at: Option, pub region: Option, pub auth_method: Option, + pub client_id_hash: Option, } impl Default for KiroCredentials { @@ -60,6 +61,7 @@ impl Default for KiroCredentials { expires_at: None, region: Some("us-east-1".to_string()), auth_method: Some("social".to_string()), + client_id_hash: None, } } } @@ -67,6 +69,8 @@ impl Default for KiroCredentials { pub struct KiroProvider { pub credentials: KiroCredentials, pub client: Client, + /// 当前加载的凭证文件路径 + pub creds_path: Option, } impl Default for KiroProvider { @@ -74,6 +78,7 @@ impl Default for KiroProvider { Self { credentials: KiroCredentials::default(), client: Client::new(), + creds_path: None, } } } @@ -112,6 +117,33 @@ impl KiroProvider { merge_credentials(&mut merged, &creds); } + // 如果有 clientIdHash,尝试加载对应的 client_id 和 client_secret + if let Some(hash) = &merged.client_id_hash { + let hash_file_path = dir.join(format!("{}.json", hash)); + tracing::info!("[KIRO] 检查 clientIdHash 文件: {}", hash_file_path.display()); + if tokio::fs::try_exists(&hash_file_path).await.unwrap_or(false) { + if let Ok(content) = tokio::fs::read_to_string(&hash_file_path).await { + if let Ok(creds) = serde_json::from_str::(&content) { + tracing::info!( + "[KIRO] Hash file {:?}: has_client_id={}, has_client_secret={}", + hash_file_path.file_name(), + creds.client_id.is_some(), + creds.client_secret.is_some() + ); + merge_credentials(&mut merged, &creds); + } else { + tracing::error!("[KIRO] 无法解析 clientIdHash 文件: {}", hash_file_path.display()); + } + } else { + tracing::error!("[KIRO] 无法读取 clientIdHash 文件: {}", hash_file_path.display()); + } + } else { + tracing::warn!("[KIRO] clientIdHash {} 指向的文件不存在: {}", hash, hash_file_path.display()); + } + } else { + tracing::info!("[KIRO] 没有 clientIdHash 字段"); + } + // 读取目录中其他 JSON 文件 if tokio::fs::try_exists(dir).await.unwrap_or(false) { let mut entries = tokio::fs::read_dir(dir).await?; @@ -144,6 +176,152 @@ impl KiroProvider { ); self.credentials = merged; + self.creds_path = Some(path); + + // 加载完成后,智能检测并更新认证方式(如果需要) + let detected_auth_method = self.detect_auth_method(); + if self.credentials.auth_method.as_deref().unwrap_or("social") != detected_auth_method { + tracing::info!("[KIRO] 加载后检测到需要调整认证方式为: {}", detected_auth_method); + self.set_auth_method(&detected_auth_method); + } + + Ok(()) + } + + /// 从指定路径加载凭证(包括 clientIdHash 文件和同目录的其他 JSON 文件) + pub async fn load_credentials_from_path( + &mut self, + path: &str, + ) -> Result<(), Box> { + let path = std::path::PathBuf::from(path); + let dir = path.parent().ok_or("Invalid path: no parent directory")?; + + let mut merged = KiroCredentials::default(); + + // 读取主凭证文件 + if tokio::fs::try_exists(&path).await.unwrap_or(false) { + let content = tokio::fs::read_to_string(&path).await?; + let creds: KiroCredentials = serde_json::from_str(&content)?; + tracing::info!( + "[KIRO] Main file loaded from {:?}: has_access={}, has_refresh={}, has_client_id={}, auth_method={:?}, clientIdHash={:?}", + path, + creds.access_token.is_some(), + creds.refresh_token.is_some(), + creds.client_id.is_some(), + creds.auth_method, + creds.client_id_hash + ); + merge_credentials(&mut merged, &creds); + } + + // 如果有 clientIdHash,尝试从 ~/.aws/sso/cache/ 目录加载对应的 client_id 和 client_secret + if let Some(hash) = &merged.client_id_hash { + // clientIdHash 文件总是在 ~/.aws/sso/cache/ 目录中 + let aws_sso_cache_dir = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".aws") + .join("sso") + .join("cache"); + let hash_file_path = aws_sso_cache_dir.join(format!("{}.json", hash)); + + tracing::debug!( + "[KIRO] 检查 clientIdHash 文件: {}", + hash_file_path.display() + ); + + if tokio::fs::try_exists(&hash_file_path).await.unwrap_or(false) { + if let Ok(content) = tokio::fs::read_to_string(&hash_file_path).await { + // 使用 serde_json::Value 来更灵活地解析,因为 hash 文件可能包含额外字段 + if let Ok(json_value) = serde_json::from_str::(&content) { + // 直接提取 clientId 和 clientSecret + let client_id = json_value + .get("clientId") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let client_secret = json_value + .get("clientSecret") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + tracing::debug!( + "[KIRO] Hash file {:?}: has_client_id={}, has_client_secret={}", + hash_file_path.file_name(), + client_id.is_some(), + client_secret.is_some() + ); + + if client_id.is_some() { + merged.client_id = client_id; + } + if client_secret.is_some() { + merged.client_secret = client_secret; + } + } else { + tracing::warn!( + "[KIRO] 无法解析 clientIdHash 文件 JSON: {}", + hash_file_path.display() + ); + } + } else { + tracing::warn!( + "[KIRO] 无法读取 clientIdHash 文件: {}", + hash_file_path.display() + ); + } + } else { + tracing::warn!( + "[KIRO] clientIdHash {} 指向的文件不存在: {}", + hash, + hash_file_path.display() + ); + } + } else { + tracing::debug!("[KIRO] 没有 clientIdHash 字段,尝试扫描同目录文件"); + } + + // 如果还没有 client_id/client_secret,读取目录中其他 JSON 文件 + if merged.client_id.is_none() || merged.client_secret.is_none() { + if tokio::fs::try_exists(dir).await.unwrap_or(false) { + let mut entries = tokio::fs::read_dir(dir).await?; + while let Some(entry) = entries.next_entry().await? { + let file_path = entry.path(); + if file_path.extension().map(|e| e == "json").unwrap_or(false) && file_path != path + { + if let Ok(content) = tokio::fs::read_to_string(&file_path).await { + if let Ok(creds) = serde_json::from_str::(&content) { + tracing::info!( + "[KIRO] Extra file {:?}: has_client_id={}, has_client_secret={}", + file_path.file_name(), + creds.client_id.is_some(), + creds.client_secret.is_some() + ); + merge_credentials(&mut merged, &creds); + } + } + } + } + } + } + + tracing::info!( + "[KIRO] Final merged from path: has_access={}, has_refresh={}, has_client_id={}, has_client_secret={}, auth_method={:?}", + merged.access_token.is_some(), + merged.refresh_token.is_some(), + merged.client_id.is_some(), + merged.client_secret.is_some(), + merged.auth_method + ); + + self.credentials = merged; + self.creds_path = Some(path); + + // 加载完成后,智能检测并更新认证方式(如果需要) + let detected_auth_method = self.detect_auth_method(); + if self.credentials.auth_method.as_deref().unwrap_or("social") != detected_auth_method { + tracing::info!("[KIRO] 从路径加载后检测到需要调整认证方式为: {}", detected_auth_method); + self.set_auth_method(&detected_auth_method); + } + Ok(()) } @@ -168,50 +346,171 @@ impl KiroProvider { } } + /// 构建健康检查使用的端点,与实际API调用保持一致 + pub fn get_health_check_url(&self) -> String { + // 重用基础URL逻辑,确保健康检查与实际API调用使用相同端点 + self.get_base_url() + } + + /// 从凭证文件中提取 region 信息的静态方法,供健康检查服务使用 + pub fn extract_region_from_creds(creds_content: &str) -> Result { + let creds: serde_json::Value = serde_json::from_str(creds_content) + .map_err(|e| format!("解析凭证失败: {}", e))?; + + let region = creds["region"] + .as_str() + .unwrap_or("us-east-1") + .to_string(); + + Ok(region) + } + + /// 构建健康检查端点的静态方法,供外部服务使用 + pub fn build_health_check_url(region: &str) -> String { + format!("https://codewhisperer.{region}.amazonaws.com/generateAssistantResponse") + } + + /// 检查 Token 是否已过期(基于时间戳) + pub fn is_token_expired(&self) -> bool { + if let Some(expires_str) = &self.credentials.expires_at { + if let Ok(expires_timestamp) = expires_str.parse::() { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + + // 提前5分钟判断为过期,避免边界情况 + return now >= (expires_timestamp - 300); + } + } + + // 如果没有过期时间信息,保守地认为可能需要刷新 + true + } + + /// 验证 refresh_token 的基本有效性 + pub fn validate_refresh_token(&self) -> Result<(), String> { + let refresh_token = self.credentials.refresh_token.as_ref() + .ok_or("缺少 refresh_token。\n💡 解决方案:\n1. 重新添加 OAuth 凭证\n2. 确保凭证文件包含完整的认证信息")?; + + // 基本格式验证 + if refresh_token.trim().is_empty() { + return Err("refresh_token 为空。\n💡 解决方案:\n1. 检查凭证文件是否损坏\n2. 重新生成 OAuth 凭证".to_string()); + } + + // 检查是否看起来像有效的 token(简单的长度和格式检查) + if refresh_token.len() < 10 { + return Err("refresh_token 格式异常(长度过短)。\n💡 解决方案:\n1. 凭证文件可能已损坏\n2. 重新获取 OAuth 凭证".to_string()); + } + + Ok(()) + } + + /// 检测最佳的认证方式 + /// 优先使用 IdC(如果有完整配置),否则回退到 social ��证 + pub fn detect_auth_method(&self) -> String { + // 检查当前设置的认证方式 + let current_auth = self.credentials.auth_method.as_deref().unwrap_or("social"); + + // 如果当前是 IdC 方式,检查是否有完整的 IdC 配置 + if current_auth.to_lowercase() == "idc" { + if self.credentials.client_id.is_some() && self.credentials.client_secret.is_some() { + // IdC 配置完整,继续使用 IdC + tracing::debug!("[KIRO] IdC 配置完整,使用 IdC 认证"); + "idc".to_string() + } else { + // IdC 配置不完整,降级到 social + tracing::warn!("[KIRO] IdC 配置不完整(缺少 client_id 或 client_secret),自动降级到 social 认证"); + "social".to_string() + } + } else { + // 默认或已设置为 social + tracing::debug!("[KIRO] 使用 social 认证"); + "social".to_string() + } + } + + /// 更新认证方式到凭证中(仅在内存中,需要调用 save_credentials 持久化) + pub fn set_auth_method(&mut self, method: &str) { + let old_method = self.credentials.auth_method.as_deref().unwrap_or("social"); + if old_method != method { + tracing::info!("[KIRO] 认证方式从 {} 切换到 {}", old_method, method); + self.credentials.auth_method = Some(method.to_string()); + } + } + pub async fn refresh_token(&mut self) -> Result> { + // 首先验证 refresh_token 的有效性 + self.validate_refresh_token()?; + + tracing::info!("[KIRO] 开始 Token 刷新流程"); + tracing::info!("[KIRO] 当前凭证状态: has_client_id={}, has_client_secret={}, auth_method={:?}", + self.credentials.client_id.is_some(), + self.credentials.client_secret.is_some(), + self.credentials.auth_method + ); + + // 先克隆必要的值,避免借用冲突 let refresh_token = self .credentials .refresh_token .as_ref() - .ok_or("No refresh token")?; + .ok_or("No refresh token")? + .clone(); - let auth_method = self - .credentials - .auth_method - .as_deref() - .unwrap_or("social") - .to_lowercase(); + // 使用智能检测的认证方式,而不是直接使用配置中的方式 + let detected_auth_method = self.detect_auth_method(); + tracing::info!("[KIRO] 检测到的认证方式: {}", detected_auth_method); + + // 如果检测到的方式与配置中的不同,更新配置 + let current_auth = self.credentials.auth_method.as_deref().unwrap_or("social"); + if current_auth != detected_auth_method { + tracing::info!("[KIRO] 认证方式从 {} 切换到 {}", current_auth, detected_auth_method); + self.set_auth_method(&detected_auth_method); + } + + let auth_method = detected_auth_method.to_lowercase(); let refresh_url = self.get_refresh_url(); + + tracing::debug!("[KIRO] refresh_token: auth_method={}, refresh_url={}", auth_method, refresh_url); + tracing::debug!("[KIRO] has_client_id={}, has_client_secret={}", + self.credentials.client_id.is_some(), + self.credentials.client_secret.is_some() + ); let resp = if auth_method == "idc" { - // AWS OIDC endpoint requires form-urlencoded format + // IdC 认证使用 JSON 格式(参考 AIClient-2-API 实现) let client_id = self .credentials .client_id .as_ref() - .ok_or("No client_id for IdC refresh")?; + .ok_or("IdC 认证配置错误:缺少 client_id。建议删除后重新添加 OAuth 凭证")?; let client_secret = self .credentials .client_secret .as_ref() - .ok_or("No client_secret for IdC refresh")?; + .ok_or("IdC 认证配置错误:缺少 client_secret。建议删除后重新添加 OAuth 凭证")?; - let form_body = format!( - "grant_type=refresh_token&client_id={}&client_secret={}&refresh_token={}", - urlencoding::encode(client_id), - urlencoding::encode(client_secret), - urlencoding::encode(refresh_token) - ); + // 使用 JSON 格式发送请求(与 AIClient-2-API 保持一致) + let body = serde_json::json!({ + "refreshToken": &refresh_token, + "clientId": client_id, + "clientSecret": client_secret, + "grantType": "refresh_token" + }); + + tracing::debug!("[KIRO] IdC 刷新请求体已构建"); self.client .post(&refresh_url) - .header("Content-Type", "application/x-www-form-urlencoded") + .header("Content-Type", "application/json") .header("Accept", "application/json") - .body(form_body) + .json(&body) .send() .await? } else { - let body = serde_json::json!({ "refreshToken": refresh_token }); + // Social 认证使用简单的 JSON 格式 + let body = serde_json::json!({ "refreshToken": &refresh_token }); self.client .post(&refresh_url) .header("Content-Type", "application/json") @@ -221,10 +520,30 @@ impl KiroProvider { .await? }; + tracing::info!("[KIRO] Token 刷新响应状态: {}", resp.status()); + if !resp.status().is_success() { let status = resp.status(); let body_text = resp.text().await.unwrap_or_default(); - return Err(format!("Refresh failed: {status} {body_text}").into()); + + tracing::warn!("[KIRO] Token 刷新失败: {} - {}", status, body_text); + + // 根据具体的HTTP状态码提供更友好的错误信息 + let error_msg = match status.as_u16() { + 401 => { + if body_text.contains("Bad credentials") || body_text.contains("invalid") { + format!("OAuth 凭证已过期或无效,需要重新认证。\n💡 解决方案:\n1. 删除当前 OAuth 凭证\n2. 重新添加 OAuth 凭证\n3. 确保使用最新的凭证文件\n\n技术详情:{} {}", status, body_text) + } else { + format!("认证失败,Token 可能已过期。\n💡 解决方案:\n1. 检查 AWS 账户状态\n2. 重新生成 OAuth 凭证\n3. 确保凭证文件格式正确\n\n技术详情:{} {}", status, body_text) + } + } + 403 => format!("权限不足,无法刷新 Token。\n💡 解决方案:\n1. 检查 AWS 账户权限\n2. 确保 OAuth 应用配置正确\n3. 联系管理员检查权限设置\n\n技术详情:{} {}", status, body_text), + 429 => format!("请求过于频繁,已被限流。\n💡 解决方案:\n1. 等待 5-10 分钟后重试\n2. 减少 Token 刷新频率\n3. 检查是否有其他程序在同时使用\n\n技术详情:{} {}", status, body_text), + 500..=599 => format!("服务器错误,AWS OAuth 服务暂时不可用。\n💡 解决方案:\n1. 稍后重试(通常几分钟后恢复)\n2. 检查 AWS 服务状态页面\n3. 如持续失败,联系 AWS 支持\n\n技术详情:{} {}", status, body_text), + _ => format!("Token 刷新失败。\n💡 解决方案:\n1. 检查网络连接\n2. 确认凭证文件完整性\n3. 尝试重新添加凭证\n\n技术详情:{} {}", status, body_text) + }; + + return Err(error_msg.into()); } let data: serde_json::Value = resp.json().await?; @@ -255,7 +574,8 @@ impl KiroProvider { } pub async fn save_credentials(&self) -> Result<(), Box> { - let path = Self::default_creds_path(); + // 使用加载时的路径或默认路径 + let path = self.creds_path.clone().unwrap_or_else(Self::default_creds_path); // 读取现有文件内容 let mut existing: serde_json::Value = if tokio::fs::try_exists(&path).await.unwrap_or(false) @@ -411,4 +731,7 @@ fn merge_credentials(target: &mut KiroCredentials, source: &KiroCredentials) { if source.auth_method.is_some() { target.auth_method = source.auth_method.clone(); } + if source.client_id_hash.is_some() { + target.client_id_hash = source.client_id_hash.clone(); + } } diff --git a/src-tauri/src/providers/openai_custom.rs b/src-tauri/src/providers/openai_custom.rs index c9ea3ad31..aecf64dc8 100644 --- a/src-tauri/src/providers/openai_custom.rs +++ b/src-tauri/src/providers/openai_custom.rs @@ -1,4 +1,5 @@ //! OpenAI Custom Provider (自定义 OpenAI 兼容 API) +use crate::models::openai::ChatCompletionRequest; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; @@ -29,6 +30,18 @@ impl OpenAICustomProvider { Self::default() } + /// 使用 API key 和 base_url 创建 Provider + pub fn with_config(api_key: String, base_url: Option) -> Self { + Self { + config: OpenAICustomConfig { + api_key: Some(api_key), + base_url, + enabled: true, + }, + client: Client::new(), + } + } + pub fn get_base_url(&self) -> String { self.config .base_url @@ -40,6 +53,32 @@ impl OpenAICustomProvider { self.config.api_key.is_some() && self.config.enabled } + /// 调用 OpenAI API(使用类型化请求) + pub async fn call_api( + &self, + request: &ChatCompletionRequest, + ) -> Result> { + let api_key = self + .config + .api_key + .as_ref() + .ok_or("OpenAI API key not configured")?; + + let base_url = self.get_base_url(); + let url = format!("{base_url}/chat/completions"); + + let resp = self + .client + .post(&url) + .header("Authorization", format!("Bearer {api_key}")) + .header("Content-Type", "application/json") + .json(request) + .send() + .await?; + + Ok(resp) + } + pub async fn chat_completions( &self, request: &serde_json::Value, diff --git a/src-tauri/src/providers/qwen.rs b/src-tauri/src/providers/qwen.rs index 78ac617ba..829969430 100644 --- a/src-tauri/src/providers/qwen.rs +++ b/src-tauri/src/providers/qwen.rs @@ -70,6 +70,16 @@ impl QwenProvider { Ok(()) } + pub async fn load_credentials_from_path( + &mut self, + path: &str, + ) -> Result<(), Box> { + let content = tokio::fs::read_to_string(path).await?; + let creds: QwenCredentials = serde_json::from_str(&content)?; + self.credentials = creds; + Ok(()) + } + pub async fn save_credentials(&self) -> Result<(), Box> { let path = Self::default_creds_path(); if let Some(parent) = path.parent() { diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs index d97afbf9e..ab1b04322 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server.rs @@ -1,17 +1,21 @@ //! HTTP API 服务器 use crate::config::Config; use crate::converter::anthropic_to_openai::convert_anthropic_to_openai; +use crate::database::DbConnection; use crate::logger::LogStore; use crate::models::anthropic::*; use crate::models::openai::*; +use crate::models::route_model::{RouteInfo, RouteListResponse}; use crate::providers::claude_custom::ClaudeCustomProvider; use crate::providers::gemini::GeminiProvider; use crate::providers::kiro::KiroProvider; use crate::providers::openai_custom::OpenAICustomProvider; use crate::providers::qwen::QwenProvider; +use crate::services::provider_pool_service::ProviderPoolService; +use crate::services::token_cache_service::TokenCacheService; use axum::{ body::Body, - extract::State, + extract::{Path, State}, http::{header, HeaderMap, StatusCode}, response::{IntoResponse, Response}, routing::{get, post}, @@ -89,6 +93,9 @@ impl ServerState { pub async fn start( &mut self, logs: Arc>, + pool_service: Arc, + token_cache: Arc, + db: Option, ) -> Result<(), Box> { if self.running { return Ok(()); @@ -106,7 +113,7 @@ impl ServerState { let kiro = self.kiro_provider.clone(); tokio::spawn(async move { - if let Err(e) = run_server(&host, port, &api_key, kiro, logs, rx).await { + if let Err(e) = run_server(&host, port, &api_key, kiro, logs, rx, pool_service, token_cache, db).await { tracing::error!("Server error: {}", e); } }); @@ -130,6 +137,7 @@ impl Clone for KiroProvider { Self { credentials: self.credentials.clone(), client: reqwest::Client::new(), + creds_path: self.creds_path.clone(), } } } @@ -138,11 +146,15 @@ impl Clone for KiroProvider { #[allow(dead_code)] struct AppState { api_key: String, + base_url: String, kiro: Arc>, logs: Arc>, kiro_refresh_lock: Arc>, gemini_refresh_lock: Arc>, qwen_refresh_lock: Arc>, + pool_service: Arc, + token_cache: Arc, + db: Option, } async fn run_server( @@ -152,22 +164,34 @@ async fn run_server( kiro: KiroProvider, logs: Arc>, shutdown: oneshot::Receiver<()>, + pool_service: Arc, + token_cache: Arc, + db: Option, ) -> Result<(), Box> { + let base_url = format!("http://{}:{}", host, port); let state = AppState { api_key: api_key.to_string(), + base_url, kiro: Arc::new(RwLock::new(kiro)), logs, kiro_refresh_lock: Arc::new(tokio::sync::Mutex::new(())), gemini_refresh_lock: Arc::new(tokio::sync::Mutex::new(())), qwen_refresh_lock: Arc::new(tokio::sync::Mutex::new(())), + pool_service, + token_cache, + db, }; let app = Router::new() .route("/health", get(health)) .route("/v1/models", get(models)) + .route("/v1/routes", get(list_routes)) .route("/v1/chat/completions", post(chat_completions)) .route("/v1/messages", post(anthropic_messages)) .route("/v1/messages/count_tokens", post(count_tokens)) + // 多供应商路由 + .route("/{selector}/v1/messages", post(anthropic_messages_with_selector)) + .route("/{selector}/v1/chat/completions", post(chat_completions_with_selector)) .with_state(state); let addr: std::net::SocketAddr = format!("{host}:{port}").parse()?; @@ -1310,3 +1334,774 @@ fn parse_bracket_tool_calls(result: &mut CWParsedResponse) { result.content = result.content.trim().to_string(); } } + +/// 列出所有可用路由 +async fn list_routes(State(state): State) -> impl IntoResponse { + let routes = match &state.db { + Some(db) => state + .pool_service + .get_available_routes(db, &state.base_url) + .unwrap_or_default(), + None => Vec::new(), + }; + + // 添加默认路由 + let mut all_routes = vec![RouteInfo { + selector: "default".to_string(), + provider_type: "kiro".to_string(), + credential_count: 1, + endpoints: vec![ + crate::models::route_model::RouteEndpoint { + path: "/v1/messages".to_string(), + protocol: "claude".to_string(), + url: format!("{}/v1/messages", state.base_url), + }, + crate::models::route_model::RouteEndpoint { + path: "/v1/chat/completions".to_string(), + protocol: "openai".to_string(), + url: format!("{}/v1/chat/completions", state.base_url), + }, + ], + tags: vec!["默认".to_string()], + enabled: true, + }]; + all_routes.extend(routes); + + let response = RouteListResponse { + base_url: state.base_url.clone(), + default_provider: "kiro".to_string(), + routes: all_routes, + }; + + Json(response) +} + +/// 带选择器的 Anthropic messages 处理 +async fn anthropic_messages_with_selector( + State(state): State, + Path(selector): Path, + headers: HeaderMap, + Json(request): Json, +) -> Response { + if let Err(e) = verify_api_key(&headers, &state.api_key).await { + state + .logs + .write() + .await + .add("warn", &format!("Unauthorized request to /{}/v1/messages", selector)); + return e.into_response(); + } + + state.logs.write().await.add( + "info", + &format!( + "[REQ] POST /{}/v1/messages model={} stream={}", + selector, request.model, request.stream + ), + ); + + // 尝试解析凭证 + let credential = match &state.db { + Some(db) => { + // 首先尝试按名称查找 + if let Ok(Some(cred)) = state.pool_service.get_by_name(db, &selector) { + Some(cred) + } + // 然后尝试按 UUID 查找 + else if let Ok(Some(cred)) = state.pool_service.get_by_uuid(db, &selector) { + Some(cred) + } + // 最后尝试按 provider 类型轮询 + else if let Ok(Some(cred)) = state + .pool_service + .select_credential(db, &selector, Some(&request.model)) + { + Some(cred) + } else { + None + } + } + None => None, + }; + + match credential { + Some(cred) => { + state.logs.write().await.add( + "info", + &format!( + "[ROUTE] Using credential: type={} name={:?} uuid={}", + cred.provider_type, + cred.name, + &cred.uuid[..8] + ), + ); + + // 根据凭证类型调用相应的 Provider + call_provider_anthropic(&state, &cred, &request).await + } + None => { + // 回退到默认 Kiro provider + state.logs.write().await.add( + "warn", + &format!( + "[ROUTE] Credential not found for selector '{}', falling back to default", + selector + ), + ); + // 调用原有的 Kiro 处理逻辑 + anthropic_messages_internal(&state, &request).await + } + } +} + +/// 带选择器的 OpenAI chat completions 处理 +async fn chat_completions_with_selector( + State(state): State, + Path(selector): Path, + headers: HeaderMap, + Json(request): Json, +) -> Response { + if let Err(e) = verify_api_key(&headers, &state.api_key).await { + state + .logs + .write() + .await + .add("warn", &format!("Unauthorized request to /{}/v1/chat/completions", selector)); + return e.into_response(); + } + + state.logs.write().await.add( + "info", + &format!( + "[REQ] POST /{}/v1/chat/completions model={} stream={}", + selector, request.model, request.stream + ), + ); + + // 尝试解析凭证 + let credential = match &state.db { + Some(db) => { + if let Ok(Some(cred)) = state.pool_service.get_by_name(db, &selector) { + Some(cred) + } else if let Ok(Some(cred)) = state.pool_service.get_by_uuid(db, &selector) { + Some(cred) + } else if let Ok(Some(cred)) = state + .pool_service + .select_credential(db, &selector, Some(&request.model)) + { + Some(cred) + } else { + None + } + } + None => None, + }; + + match credential { + Some(cred) => { + state.logs.write().await.add( + "info", + &format!( + "[ROUTE] Using credential: type={} name={:?} uuid={}", + cred.provider_type, + cred.name, + &cred.uuid[..8] + ), + ); + + call_provider_openai(&state, &cred, &request).await + } + None => { + state.logs.write().await.add( + "warn", + &format!( + "[ROUTE] Credential not found for selector '{}', falling back to default", + selector + ), + ); + chat_completions_internal(&state, &request).await + } + } +} + +/// 内部 Anthropic messages 处理 (使用默认 Kiro) +async fn anthropic_messages_internal( + state: &AppState, + request: &AnthropicMessagesRequest, +) -> Response { + // 检查 token + { + let _guard = state.kiro_refresh_lock.lock().await; + let mut kiro = state.kiro.write().await; + let needs_refresh = + kiro.credentials.access_token.is_none() || kiro.is_token_expiring_soon(); + if needs_refresh { + if let Err(e) = kiro.refresh_token().await { + state + .logs + .write() + .await + .add("error", &format!("[AUTH] Token refresh failed: {e}")); + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"error": {"message": format!("Token refresh failed: {e}")}})), + ) + .into_response(); + } + } + } + + let openai_request = convert_anthropic_to_openai(request); + let kiro = state.kiro.read().await; + + match kiro.call_api(&openai_request).await { + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + match resp.bytes().await { + Ok(bytes) => { + let body = String::from_utf8_lossy(&bytes).to_string(); + let parsed = parse_cw_response(&body); + if request.stream { + build_anthropic_stream_response(&request.model, &parsed) + } else { + build_anthropic_response(&request.model, &parsed) + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } else { + let body = resp.text().await.unwrap_or_default(); + ( + StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + Json(serde_json::json!({"error": {"message": format!("Upstream error: {}", body)}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } +} + +/// 内部 OpenAI chat completions 处理 (使用默认 Kiro) +async fn chat_completions_internal(state: &AppState, request: &ChatCompletionRequest) -> Response { + { + let _guard = state.kiro_refresh_lock.lock().await; + let mut kiro = state.kiro.write().await; + let needs_refresh = + kiro.credentials.access_token.is_none() || kiro.is_token_expiring_soon(); + if needs_refresh { + if let Err(e) = kiro.refresh_token().await { + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"error": {"message": format!("Token refresh failed: {e}")}})), + ) + .into_response(); + } + } + } + + let kiro = state.kiro.read().await; + match kiro.call_api(request).await { + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + match resp.text().await { + Ok(body) => { + let parsed = parse_cw_response(&body); + let has_tool_calls = !parsed.tool_calls.is_empty(); + + let message = if has_tool_calls { + serde_json::json!({ + "role": "assistant", + "content": if parsed.content.is_empty() { serde_json::Value::Null } else { serde_json::json!(parsed.content) }, + "tool_calls": parsed.tool_calls.iter().map(|tc| { + serde_json::json!({ + "id": tc.id, + "type": "function", + "function": { + "name": tc.function.name, + "arguments": tc.function.arguments + } + }) + }).collect::>() + }) + } else { + serde_json::json!({ + "role": "assistant", + "content": parsed.content + }) + }; + + let response = serde_json::json!({ + "id": format!("chatcmpl-{}", uuid::Uuid::new_v4()), + "object": "chat.completion", + "created": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + "model": request.model, + "choices": [{ + "index": 0, + "message": message, + "finish_reason": if has_tool_calls { "tool_calls" } else { "stop" } + }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + } + }); + Json(response).into_response() + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } else { + let body = resp.text().await.unwrap_or_default(); + ( + StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + Json(serde_json::json!({"error": {"message": format!("Upstream error: {}", body)}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } +} + +use crate::models::provider_pool_model::{CredentialData, ProviderCredential}; + +/// 根据凭证调用 Provider (Anthropic 格式) +async fn call_provider_anthropic( + state: &AppState, + credential: &ProviderCredential, + request: &AnthropicMessagesRequest, +) -> Response { + match &credential.credential { + CredentialData::KiroOAuth { creds_file_path } => { + // 使用 TokenCacheService 获取有效 token + let db = match &state.db { + Some(db) => db, + None => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": "Database not available"}})), + ) + .into_response(); + } + }; + + // 获取缓存的 token + let token = match state.token_cache.get_valid_token(db, &credential.uuid).await { + Ok(t) => t, + Err(e) => { + tracing::warn!("[POOL] Token cache miss, loading from source: {}", e); + // 回退到从源文件加载 + let mut kiro = KiroProvider::new(); + if let Err(e) = kiro.load_credentials_from_path(creds_file_path).await { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": format!("Failed to load Kiro credentials: {}", e)}})), + ) + .into_response(); + } + if let Err(e) = kiro.refresh_token().await { + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"error": {"message": format!("Token refresh failed: {}", e)}})), + ) + .into_response(); + } + kiro.credentials.access_token.unwrap_or_default() + } + }; + + // 使用获取到的 token 创建 KiroProvider + let mut kiro = KiroProvider::new(); + kiro.credentials.access_token = Some(token); + // 从源文件加载其他配置(region, profile_arn 等) + let _ = kiro.load_credentials_from_path(creds_file_path).await; + + let openai_request = convert_anthropic_to_openai(request); + let resp = match kiro.call_api(&openai_request).await { + Ok(r) => r, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(); + } + }; + + let status = resp.status(); + if status.is_success() { + match resp.bytes().await { + Ok(bytes) => { + let body = String::from_utf8_lossy(&bytes).to_string(); + let parsed = parse_cw_response(&body); + if request.stream { + build_anthropic_stream_response(&request.model, &parsed) + } else { + build_anthropic_response(&request.model, &parsed) + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } else if status.as_u16() == 401 || status.as_u16() == 403 { + // Token 过期,强制刷新并重试 + tracing::info!("[POOL] Got {}, forcing token refresh for {}", status, &credential.uuid[..8]); + + let new_token = match state.token_cache.refresh_and_cache(db, &credential.uuid, true).await { + Ok(t) => t, + Err(e) => { + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"error": {"message": format!("Token refresh failed: {}", e)}})), + ) + .into_response(); + } + }; + + // 使用新 token 重试 + kiro.credentials.access_token = Some(new_token); + match kiro.call_api(&openai_request).await { + Ok(retry_resp) => { + if retry_resp.status().is_success() { + match retry_resp.bytes().await { + Ok(bytes) => { + let body = String::from_utf8_lossy(&bytes).to_string(); + let parsed = parse_cw_response(&body); + if request.stream { + build_anthropic_stream_response(&request.model, &parsed) + } else { + build_anthropic_response(&request.model, &parsed) + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } else { + let body = retry_resp.text().await.unwrap_or_default(); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": format!("Retry failed: {}", body)}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } else { + let body = resp.text().await.unwrap_or_default(); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": body}})), + ) + .into_response() + } + } + CredentialData::GeminiOAuth { .. } => { + // Gemini OAuth 路由暂不支持 + ( + StatusCode::NOT_IMPLEMENTED, + Json(serde_json::json!({"error": {"message": "Gemini OAuth routing not yet implemented. Use /v1/messages with Gemini models instead."}})), + ) + .into_response() + } + CredentialData::QwenOAuth { .. } => { + // Qwen OAuth 路由暂不支持 + ( + StatusCode::NOT_IMPLEMENTED, + Json(serde_json::json!({"error": {"message": "Qwen OAuth routing not yet implemented. Use /v1/messages with Qwen models instead."}})), + ) + .into_response() + } + CredentialData::OpenAIKey { api_key, base_url } => { + let openai = OpenAICustomProvider::with_config(api_key.clone(), base_url.clone()); + let openai_request = convert_anthropic_to_openai(request); + match openai.call_api(&openai_request).await { + Ok(resp) => { + if resp.status().is_success() { + match resp.text().await { + Ok(body) => { + if let Ok(openai_resp) = serde_json::from_str::(&body) { + let content = openai_resp["choices"][0]["message"]["content"] + .as_str() + .unwrap_or(""); + let parsed = CWParsedResponse { + content: content.to_string(), + tool_calls: Vec::new(), + usage_credits: 0.0, + context_usage_percentage: 0.0, + }; + if request.stream { + build_anthropic_stream_response(&request.model, &parsed) + } else { + build_anthropic_response(&request.model, &parsed) + } + } else { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": "Failed to parse OpenAI response"}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } else { + let body = resp.text().await.unwrap_or_default(); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": body}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } + CredentialData::ClaudeKey { api_key, base_url } => { + let claude = ClaudeCustomProvider::with_config(api_key.clone(), base_url.clone()); + match claude.call_api(request).await { + Ok(resp) => { + let status = resp.status(); + match resp.text().await { + Ok(body) => { + if status.is_success() { + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from(body)) + .unwrap_or_else(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": "Failed to build response"}})), + ) + .into_response() + }) + } else { + ( + StatusCode::from_u16(status.as_u16()) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + Json(serde_json::json!({"error": {"message": body}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } + } +} + +/// 根据凭证调用 Provider (OpenAI 格式) +async fn call_provider_openai( + _state: &AppState, + credential: &ProviderCredential, + request: &ChatCompletionRequest, +) -> Response { + match &credential.credential { + CredentialData::KiroOAuth { creds_file_path } => { + let mut kiro = KiroProvider::new(); + if let Err(e) = kiro.load_credentials_from_path(creds_file_path).await { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": format!("Failed to load Kiro credentials: {}", e)}})), + ) + .into_response(); + } + if let Err(e) = kiro.refresh_token().await { + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"error": {"message": format!("Token refresh failed: {}", e)}})), + ) + .into_response(); + } + + match kiro.call_api(request).await { + Ok(resp) => { + if resp.status().is_success() { + match resp.text().await { + Ok(body) => { + let parsed = parse_cw_response(&body); + let has_tool_calls = !parsed.tool_calls.is_empty(); + + let message = if has_tool_calls { + serde_json::json!({ + "role": "assistant", + "content": if parsed.content.is_empty() { serde_json::Value::Null } else { serde_json::json!(parsed.content) }, + "tool_calls": parsed.tool_calls.iter().map(|tc| { + serde_json::json!({ + "id": tc.id, + "type": "function", + "function": { + "name": tc.function.name, + "arguments": tc.function.arguments + } + }) + }).collect::>() + }) + } else { + serde_json::json!({ + "role": "assistant", + "content": parsed.content + }) + }; + + Json(serde_json::json!({ + "id": format!("chatcmpl-{}", uuid::Uuid::new_v4()), + "object": "chat.completion", + "created": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + "model": request.model, + "choices": [{ + "index": 0, + "message": message, + "finish_reason": if has_tool_calls { "tool_calls" } else { "stop" } + }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + } + })) + .into_response() + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } else { + let body = resp.text().await.unwrap_or_default(); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": body}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } + CredentialData::GeminiOAuth { .. } => { + ( + StatusCode::NOT_IMPLEMENTED, + Json(serde_json::json!({"error": {"message": "Gemini OAuth routing not yet implemented."}})), + ) + .into_response() + } + CredentialData::QwenOAuth { .. } => { + ( + StatusCode::NOT_IMPLEMENTED, + Json(serde_json::json!({"error": {"message": "Qwen OAuth routing not yet implemented."}})), + ) + .into_response() + } + CredentialData::OpenAIKey { api_key, base_url } => { + let openai = OpenAICustomProvider::with_config(api_key.clone(), base_url.clone()); + match openai.call_api(request).await { + Ok(resp) => { + if resp.status().is_success() { + match resp.text().await { + Ok(body) => { + if let Ok(json) = serde_json::from_str::(&body) { + Json(json).into_response() + } else { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": "Invalid JSON response"}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } else { + let body = resp.text().await.unwrap_or_default(); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": body}})), + ) + .into_response() + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } + CredentialData::ClaudeKey { api_key, base_url } => { + let claude = ClaudeCustomProvider::with_config(api_key.clone(), base_url.clone()); + match claude.call_openai_api(request).await { + Ok(resp) => Json(resp).into_response(), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), + } + } + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index aca3ef533..90b1adac9 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -3,5 +3,7 @@ pub mod mcp_service; pub mod mcp_sync; pub mod prompt_service; pub mod prompt_sync; +pub mod provider_pool_service; pub mod skill_service; pub mod switch; +pub mod token_cache_service; diff --git a/src-tauri/src/services/provider_pool_service.rs b/src-tauri/src/services/provider_pool_service.rs new file mode 100644 index 000000000..daa684218 --- /dev/null +++ b/src-tauri/src/services/provider_pool_service.rs @@ -0,0 +1,852 @@ +//! Provider Pool 管理服务 +//! +//! 提供凭证池的选择、健康检测、负载均衡等功能。 + +use crate::database::dao::provider_pool::ProviderPoolDao; +use crate::database::DbConnection; +use crate::models::provider_pool_model::{ + get_default_check_model, get_oauth_creds_path, CredentialData, CredentialDisplay, + HealthCheckResult, OAuthStatus, PoolProviderType, PoolStats, ProviderCredential, + ProviderPoolOverview, +}; +use crate::models::route_model::RouteInfo; +use crate::providers::kiro::KiroProvider; +use chrono::Utc; +use reqwest::Client; +use std::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +/// 凭证池管理服务 +pub struct ProviderPoolService { + /// HTTP 客户端(用于健康检测) + client: Client, + /// 轮询索引(按 provider_type 和可选的 model 分组) + round_robin_index: std::sync::RwLock>, + /// 最大错误次数(超过后标记为不健康) + max_error_count: u32, + /// 健康检查超时时间 + health_check_timeout: Duration, +} + +impl Default for ProviderPoolService { + fn default() -> Self { + Self::new() + } +} + +impl ProviderPoolService { + pub fn new() -> Self { + Self { + client: Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap_or_default(), + round_robin_index: std::sync::RwLock::new(HashMap::new()), + max_error_count: 3, + health_check_timeout: Duration::from_secs(30), + } + } + + /// 获取所有凭证概览 + pub fn get_overview(&self, db: &DbConnection) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + let grouped = ProviderPoolDao::get_grouped(&conn).map_err(|e| e.to_string())?; + + let mut overview = Vec::new(); + for (provider_type, mut credentials) in grouped { + // 为每个凭证加载 token 缓存 + for cred in &mut credentials { + cred.cached_token = ProviderPoolDao::get_token_cache(&conn, &cred.uuid) + .ok() + .flatten(); + } + + let stats = PoolStats::from_credentials(&credentials); + let displays: Vec = credentials.iter().map(|c| c.into()).collect(); + + overview.push(ProviderPoolOverview { + provider_type: provider_type.to_string(), + stats, + credentials: displays, + }); + } + + // 按 provider_type 排序 + overview.sort_by(|a, b| a.provider_type.cmp(&b.provider_type)); + Ok(overview) + } + + /// 获取指定类型的凭证列表 + pub fn get_by_type( + &self, + db: &DbConnection, + provider_type: &str, + ) -> Result, String> { + let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; + let conn = db.lock().map_err(|e| e.to_string())?; + let mut credentials = ProviderPoolDao::get_by_type(&conn, &pt).map_err(|e| e.to_string())?; + + // 为每个凭证加载 token 缓存 + for cred in &mut credentials { + cred.cached_token = ProviderPoolDao::get_token_cache(&conn, &cred.uuid) + .ok() + .flatten(); + } + + Ok(credentials.iter().map(|c| c.into()).collect()) + } + + /// 添加凭证 + pub fn add_credential( + &self, + db: &DbConnection, + provider_type: &str, + credential: CredentialData, + name: Option, + check_health: Option, + check_model_name: Option, + ) -> Result { + let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; + + let mut cred = ProviderCredential::new(pt, credential); + cred.name = name; + cred.check_health = check_health.unwrap_or(true); + cred.check_model_name = check_model_name; + + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::insert(&conn, &cred).map_err(|e| e.to_string())?; + + Ok(cred) + } + + /// 更新凭证 + pub fn update_credential( + &self, + db: &DbConnection, + uuid: &str, + name: Option, + is_disabled: Option, + check_health: Option, + check_model_name: Option, + not_supported_models: Option>, + ) -> Result { + let conn = db.lock().map_err(|e| e.to_string())?; + let mut cred = ProviderPoolDao::get_by_uuid(&conn, uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Credential not found: {}", uuid))?; + + if let Some(n) = name { + cred.name = Some(n); + } + if let Some(d) = is_disabled { + cred.is_disabled = d; + } + if let Some(c) = check_health { + cred.check_health = c; + } + if let Some(m) = check_model_name { + cred.check_model_name = Some(m); + } + if let Some(models) = not_supported_models { + cred.not_supported_models = models; + } + cred.updated_at = Utc::now(); + + ProviderPoolDao::update(&conn, &cred).map_err(|e| e.to_string())?; + Ok(cred) + } + + /// 删除凭证 + pub fn delete_credential(&self, db: &DbConnection, uuid: &str) -> Result { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::delete(&conn, uuid).map_err(|e| e.to_string()) + } + + /// 选择一个可用的凭证(轮询负载均衡) + pub fn select_credential( + &self, + db: &DbConnection, + provider_type: &str, + model: Option<&str>, + ) -> Result, String> { + let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; + let conn = db.lock().map_err(|e| e.to_string())?; + let credentials = ProviderPoolDao::get_by_type(&conn, &pt).map_err(|e| e.to_string())?; + drop(conn); + + // 过滤可用的凭证 + let mut available: Vec<_> = credentials + .into_iter() + .filter(|c| c.is_available()) + .collect(); + + // 如果指定了模型,进一步过滤支持该模型的凭证 + if let Some(m) = model { + available.retain(|c| c.supports_model(m)); + } + + if available.is_empty() { + return Ok(None); + } + + // 轮询选择 + let index_key = match model { + Some(m) => format!("{}:{}", provider_type, m), + None => provider_type.to_string(), + }; + + let index = { + let indices = self.round_robin_index.read().unwrap(); + indices + .get(&index_key) + .map(|i| i.load(Ordering::SeqCst)) + .unwrap_or(0) + }; + + let selected_index = index % available.len(); + let selected = available.remove(selected_index); + + // 更新轮询索引 + { + let mut indices = self.round_robin_index.write().unwrap(); + let counter = indices + .entry(index_key) + .or_insert_with(|| AtomicUsize::new(0)); + counter.store((index + 1) % usize::MAX, Ordering::SeqCst); + } + + Ok(Some(selected)) + } + + /// 记录凭证使用 + pub fn record_usage(&self, db: &DbConnection, uuid: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + let cred = ProviderPoolDao::get_by_uuid(&conn, uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Credential not found: {}", uuid))?; + + ProviderPoolDao::update_usage(&conn, uuid, cred.usage_count + 1, Utc::now()) + .map_err(|e| e.to_string()) + } + + /// 标记凭证为健康 + pub fn mark_healthy( + &self, + db: &DbConnection, + uuid: &str, + check_model: Option<&str>, + ) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::update_health_status( + &conn, + uuid, + true, + 0, + None, + None, + Some(Utc::now()), + check_model, + ) + .map_err(|e| e.to_string()) + } + + /// 标记凭证为不健康 + pub fn mark_unhealthy( + &self, + db: &DbConnection, + uuid: &str, + error_message: Option<&str>, + ) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + let cred = ProviderPoolDao::get_by_uuid(&conn, uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Credential not found: {}", uuid))?; + + let new_error_count = cred.error_count + 1; + let is_healthy = new_error_count < self.max_error_count; + + ProviderPoolDao::update_health_status( + &conn, + uuid, + is_healthy, + new_error_count, + Some(Utc::now()), + error_message, + None, + None, + ) + .map_err(|e| e.to_string()) + } + + /// 重置凭证计数器 + pub fn reset_counters(&self, db: &DbConnection, uuid: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::reset_counters(&conn, uuid).map_err(|e| e.to_string()) + } + + /// 重置指定类型的所有凭证健康状态 + pub fn reset_health_by_type( + &self, + db: &DbConnection, + provider_type: &str, + ) -> Result { + let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::reset_health_by_type(&conn, &pt).map_err(|e| e.to_string()) + } + + /// 执行单个凭证的健康检查 + pub async fn check_credential_health( + &self, + db: &DbConnection, + uuid: &str, + ) -> Result { + let cred = { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_by_uuid(&conn, uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Credential not found: {}", uuid))? + }; + + let check_model = cred + .check_model_name + .clone() + .unwrap_or_else(|| get_default_check_model(cred.provider_type).to_string()); + + let start = std::time::Instant::now(); + let result = self + .perform_health_check(&cred.credential, &check_model) + .await; + let duration_ms = start.elapsed().as_millis() as u64; + + match result { + Ok(_) => { + self.mark_healthy(db, uuid, Some(&check_model))?; + Ok(HealthCheckResult { + uuid: uuid.to_string(), + success: true, + model: Some(check_model), + message: Some("Health check passed".to_string()), + duration_ms, + }) + } + Err(e) => { + self.mark_unhealthy(db, uuid, Some(&e))?; + Ok(HealthCheckResult { + uuid: uuid.to_string(), + success: false, + model: Some(check_model), + message: Some(e), + duration_ms, + }) + } + } + } + + /// 执行指定类型的所有凭证健康检查 + pub async fn check_type_health( + &self, + db: &DbConnection, + provider_type: &str, + ) -> Result, String> { + let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; + let credentials = { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_by_type(&conn, &pt).map_err(|e| e.to_string())? + }; + + let mut results = Vec::new(); + for cred in credentials { + if cred.is_disabled || !cred.check_health { + continue; + } + + let result = self.check_credential_health(db, &cred.uuid).await?; + results.push(result); + } + + Ok(results) + } + + /// 执行实际的健康检查请求 + async fn perform_health_check( + &self, + credential: &CredentialData, + model: &str, + ) -> Result<(), String> { + // 根据凭证类型构建测试请求 + match credential { + CredentialData::KiroOAuth { creds_file_path } => { + self.check_kiro_health(creds_file_path, model).await + } + CredentialData::GeminiOAuth { + creds_file_path, + project_id, + } => { + self.check_gemini_health(creds_file_path, project_id.as_deref(), model) + .await + } + CredentialData::QwenOAuth { creds_file_path } => { + self.check_qwen_health(creds_file_path, model).await + } + CredentialData::OpenAIKey { api_key, base_url } => { + self.check_openai_health(api_key, base_url.as_deref(), model) + .await + } + CredentialData::ClaudeKey { api_key, base_url } => { + self.check_claude_health(api_key, base_url.as_deref(), model) + .await + } + } + } + + /// 将技术错误转换为用户友好的错误信息 + fn format_user_friendly_error(&self, error: &str, provider_type: &str) -> String { + if error.contains("No client_id") { + format!("OAuth 配置不完整:缺少必要的认证参数。\n💡 解决方案:\n1. 检查 {} OAuth 凭证配置是否完整\n2. 如问题持续,建议删除后重新添加此凭证\n3. 或者切换到其他可用的凭证", provider_type) + } else if error.contains("请求失败") || error.contains("error sending request") { + format!("网络连接失败,无法访问 {} 服务。\n💡 解决方案:\n1. 检查网络连接是否正常\n2. 确认防火墙或代理设置\n3. 稍后重试,如问题持续请联系网络管理员", provider_type) + } else if error.contains("HTTP 401") || error.contains("HTTP 403") { + format!("{} 认证失败,凭证可能已过期或无效。\n💡 解决方案:\n1. 点击\"刷新\"按钮尝试更新 Token\n2. 如刷新失败,请删除后重新添加此凭证\n3. 检查账户权限是否正常", provider_type) + } else if error.contains("HTTP 429") { + format!("{} 请求频率过高,已被限流。\n💡 解决方案:\n1. 稍等几分钟后再次尝试\n2. 考虑添加更多凭证分散负载", provider_type) + } else if error.contains("HTTP 500") || error.contains("HTTP 502") || error.contains("HTTP 503") { + format!("{} 服务暂时不可用。\n💡 解决方案:\n1. 这通常是服务提供方的临时问题\n2. 请稍后重试\n3. 如问题持续,可尝试其他凭证", provider_type) + } else if error.contains("读取凭证文件失败") || error.contains("解析凭证失败") { + format!("凭证文件损坏或不可读。\n💡 解决方案:\n1. 凭证文件可能已损坏\n2. 建议删除此凭证后重新添加\n3. 确保文件权限正确且格式为有效的 JSON") + } else { + // 对于其他未识别的错误,提供通用建议 + format!("操作失败:{}\n💡 建议:\n1. 检查网络连接和凭证状态\n2. 尝试刷新 Token 或重新添加凭证\n3. 如问题持续,请联系技术支持", error) + } + } + + // Kiro OAuth 健康检查 + async fn check_kiro_health(&self, creds_path: &str, model: &str) -> Result<(), String> { + tracing::debug!("[KIRO HEALTH] 开始健康检查,凭证路径: {}", creds_path); + + // 使用 KiroProvider 加载凭证(包括 clientIdHash 文件) + let mut provider = KiroProvider::new(); + provider.load_credentials_from_path(creds_path).await + .map_err(|e| self.format_user_friendly_error(&format!("加载凭证失败: {}", e), "Kiro"))?; + + let access_token = provider.credentials.access_token.as_ref() + .ok_or_else(|| "凭证中缺少 access_token".to_string())?; + + let health_check_url = provider.get_health_check_url(); + + // 获取 modelId 映射 + let model_id = match model { + "claude-opus-4-5" | "claude-opus-4-5-20251101" => "claude-opus-4.5", + "claude-haiku-4-5" => "claude-haiku-4.5", + "claude-sonnet-4-5" | "claude-sonnet-4-5-20250929" => "CLAUDE_SONNET_4_5_20250929_V1_0", + "claude-sonnet-4-20250514" => "CLAUDE_SONNET_4_20250514_V1_0", + "claude-3-7-sonnet-20250219" => "CLAUDE_3_7_SONNET_20250219_V1_0", + _ => "claude-haiku-4.5", // 默认使用 haiku + }; + + tracing::debug!("[KIRO HEALTH] 健康检查 URL: {}", health_check_url); + tracing::debug!("[KIRO HEALTH] 使用模型: {} -> {}", model, model_id); + + // 构建与实际 API 调用相同格式的测试请求(参考 AIClient-2-API 实现) + let conversation_id = uuid::Uuid::new_v4().to_string(); + let mut request_body = serde_json::json!({ + "conversationState": { + "chatTriggerType": "MANUAL", + "conversationId": conversation_id, + "currentMessage": { + "userInputMessage": { + "content": "Say OK", + "modelId": model_id, + "origin": "AI_EDITOR" + } + } + } + }); + + // 如果是 social 认证方式,需要添加 profileArn + if provider.credentials.auth_method.as_deref() == Some("social") { + if let Some(profile_arn) = &provider.credentials.profile_arn { + request_body["profileArn"] = serde_json::json!(profile_arn); + } + } + + tracing::debug!("[KIRO HEALTH] 请求体已构建"); + + let response = self + .client + .post(&health_check_url) + .bearer_auth(access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("x-amz-user-agent", "aws-sdk-js/1.0.7 KiroIDE-0.1.25") + .header("user-agent", "aws-sdk-js/1.0.7 ua/2.1 os/macos#14.0 lang/js md/nodejs#20.16.0 api/codewhispererstreaming#1.0.7 m/E KiroIDE-0.1.25") + .header("amz-sdk-invocation-id", uuid::Uuid::new_v4().to_string()) + .header("amz-sdk-request", "attempt=1; max=1") + .header("x-amzn-kiro-agent-mode", "vibe") + .json(&request_body) + .timeout(self.health_check_timeout) + .send() + .await + .map_err(|e| self.format_user_friendly_error(&format!("请求失败: {}", e), "Kiro"))?; + + let status = response.status(); + tracing::info!("[KIRO HEALTH] 响应状态: {}", status); + + if status.is_success() { + tracing::info!("[KIRO HEALTH] 健康检查成功"); + Ok(()) + } else { + let body_text = response.text().await.unwrap_or_default(); + tracing::warn!("[KIRO HEALTH] 健康检查失败: {} - {}", status, body_text); + let error_msg = format!("HTTP {}: {}", status, body_text); + Err(self.format_user_friendly_error(&error_msg, "Kiro")) + } + } + + // Gemini OAuth 健康检查 + async fn check_gemini_health( + &self, + creds_path: &str, + _project_id: Option<&str>, + model: &str, + ) -> Result<(), String> { + let creds_content = + std::fs::read_to_string(creds_path).map_err(|e| format!("读取凭证文件失败: {}", e))?; + let creds: serde_json::Value = + serde_json::from_str(&creds_content).map_err(|e| format!("解析凭证失败: {}", e))?; + + let access_token = creds["access_token"] + .as_str() + .ok_or_else(|| "凭证中缺少 access_token".to_string())?; + + let url = format!( + "https://generativelanguage.googleapis.com/v1beta/models/{}:generateContent", + model + ); + + let request_body = serde_json::json!({ + "contents": [{ + "parts": [{"text": "Say OK"}] + }], + "generationConfig": { + "maxOutputTokens": 10 + } + }); + + let response = self + .client + .post(&url) + .bearer_auth(access_token) + .json(&request_body) + .timeout(self.health_check_timeout) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + + if response.status().is_success() { + Ok(()) + } else { + Err(format!("HTTP {}", response.status())) + } + } + + // Qwen OAuth 健康检查 + async fn check_qwen_health(&self, creds_path: &str, model: &str) -> Result<(), String> { + let creds_content = + std::fs::read_to_string(creds_path).map_err(|e| format!("读取凭证文件失败: {}", e))?; + let creds: serde_json::Value = + serde_json::from_str(&creds_content).map_err(|e| format!("解析凭证失败: {}", e))?; + + let access_token = creds["access_token"] + .as_str() + .ok_or_else(|| "凭证中缺少 access_token".to_string())?; + + let request_body = serde_json::json!({ + "model": model, + "messages": [{"role": "user", "content": "Say OK"}], + "max_tokens": 10 + }); + + let response = self + .client + .post("https://chat.qwen.ai/api/v1/chat/completions") + .bearer_auth(access_token) + .json(&request_body) + .timeout(self.health_check_timeout) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + + if response.status().is_success() { + Ok(()) + } else { + Err(format!("HTTP {}", response.status())) + } + } + + // OpenAI API 健康检查 + async fn check_openai_health( + &self, + api_key: &str, + base_url: Option<&str>, + model: &str, + ) -> Result<(), String> { + let url = format!( + "{}/chat/completions", + base_url.unwrap_or("https://api.openai.com/v1") + ); + + let request_body = serde_json::json!({ + "model": model, + "messages": [{"role": "user", "content": "Say OK"}], + "max_tokens": 10 + }); + + let response = self + .client + .post(&url) + .bearer_auth(api_key) + .json(&request_body) + .timeout(self.health_check_timeout) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + + if response.status().is_success() { + Ok(()) + } else { + Err(format!("HTTP {}", response.status())) + } + } + + // Claude API 健康检查 + async fn check_claude_health( + &self, + api_key: &str, + base_url: Option<&str>, + model: &str, + ) -> Result<(), String> { + let url = format!( + "{}/messages", + base_url.unwrap_or("https://api.anthropic.com/v1") + ); + + let request_body = serde_json::json!({ + "model": model, + "messages": [{"role": "user", "content": "Say OK"}], + "max_tokens": 10 + }); + + let response = self + .client + .post(&url) + .header("x-api-key", api_key) + .header("anthropic-version", "2023-06-01") + .json(&request_body) + .timeout(self.health_check_timeout) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + + if response.status().is_success() { + Ok(()) + } else { + Err(format!("HTTP {}", response.status())) + } + } + + /// 根据名称获取凭证 + pub fn get_by_name( + &self, + db: &DbConnection, + name: &str, + ) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_by_name(&conn, name).map_err(|e| e.to_string()) + } + + /// 根据 UUID 获取凭证 + pub fn get_by_uuid( + &self, + db: &DbConnection, + uuid: &str, + ) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_by_uuid(&conn, uuid).map_err(|e| e.to_string()) + } + + /// 获取所有可用的路由端点 + pub fn get_available_routes( + &self, + db: &DbConnection, + base_url: &str, + ) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + let grouped = ProviderPoolDao::get_grouped(&conn).map_err(|e| e.to_string())?; + drop(conn); + + let mut routes = Vec::new(); + + // 为每种 Provider 类型创建路由 + for (provider_type, credentials) in &grouped { + let available: Vec<_> = credentials.iter().filter(|c| c.is_available()).collect(); + if available.is_empty() { + continue; + } + + // Provider 类型路由 (轮询) + let mut route = RouteInfo::new(provider_type.to_string(), provider_type.to_string()); + route.credential_count = available.len(); + route.add_endpoint(base_url, "claude"); + route.add_endpoint(base_url, "openai"); + route.tags.push("轮询".to_string()); + routes.push(route); + } + + // 为每个命名凭证创建路由 + for (_provider_type, credentials) in &grouped { + for cred in credentials { + if let Some(name) = &cred.name { + if cred.is_available() { + let mut route = + RouteInfo::new(name.clone(), cred.provider_type.to_string()); + route.credential_count = 1; + route.enabled = !cred.is_disabled; + route.add_endpoint(base_url, "claude"); + route.add_endpoint(base_url, "openai"); + route.tags.push("指定凭证".to_string()); + routes.push(route); + } + } + } + } + + Ok(routes) + } + + /// 获取 OAuth 凭证状态 + pub fn get_oauth_status(&self, creds_path: &str, provider_type: &str) -> Result { + let content = std::fs::read_to_string(creds_path) + .map_err(|e| format!("读取凭证文件失败: {}", e))?; + let creds: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| format!("解析凭证文件失败: {}", e))?; + + let has_access_token = creds.get("accessToken") + .or_else(|| creds.get("access_token")) + .map(|v| v.as_str().is_some()) + .unwrap_or(false); + + let has_refresh_token = creds.get("refreshToken") + .or_else(|| creds.get("refresh_token")) + .map(|v| v.as_str().is_some()) + .unwrap_or(false); + + // 检查 token 是否有效(根据 expiry_date 判断) + let (is_token_valid, expiry_info) = match provider_type { + "kiro" => { + let expires_at = creds.get("expiresAt") + .or_else(|| creds.get("expires_at")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + // Kiro 没有标准的过期时间字段,假设有 access_token 就有效 + (has_access_token, expires_at) + } + "gemini" | "qwen" => { + let expiry = creds.get("expiry_date").and_then(|v| v.as_i64()); + if let Some(exp) = expiry { + let now = chrono::Utc::now().timestamp(); + let is_valid = exp > now; + let expiry_str = chrono::DateTime::from_timestamp(exp, 0) + .map(|dt| dt.to_rfc3339()) + .unwrap_or_else(|| exp.to_string()); + (is_valid, Some(expiry_str)) + } else { + (has_access_token, None) + } + } + _ => (has_access_token, None), + }; + + Ok(OAuthStatus { + has_access_token, + has_refresh_token, + is_token_valid, + expiry_info, + creds_path: creds_path.to_string(), + }) + } + + /// 刷新 OAuth Token (Kiro) + pub async fn refresh_kiro_token(&self, creds_path: &str) -> Result { + let mut provider = crate::providers::kiro::KiroProvider::new(); + provider.load_credentials_from_path(creds_path).await + .map_err(|e| self.format_user_friendly_error(&format!("加载凭证失败: {}", e), "Kiro"))?; + provider.refresh_token().await + .map_err(|e| self.format_user_friendly_error(&format!("刷新 Token 失败: {}", e), "Kiro")) + } + + /// 刷新 OAuth Token (Gemini) + pub async fn refresh_gemini_token(&self, creds_path: &str) -> Result { + let mut provider = crate::providers::gemini::GeminiProvider::new(); + provider.load_credentials_from_path(creds_path).await + .map_err(|e| format!("加载凭证失败: {}", e))?; + provider.refresh_token().await + .map_err(|e| format!("刷新 Token 失败: {}", e)) + } + + /// 刷新 OAuth Token (Qwen) + pub async fn refresh_qwen_token(&self, creds_path: &str) -> Result { + let mut provider = crate::providers::qwen::QwenProvider::new(); + provider.load_credentials_from_path(creds_path).await + .map_err(|e| format!("加载凭证失败: {}", e))?; + provider.refresh_token().await + .map_err(|e| format!("刷新 Token 失败: {}", e)) + } + + /// 刷新凭证池中指定凭证的 OAuth Token + pub async fn refresh_credential_token( + &self, + db: &DbConnection, + uuid: &str, + ) -> Result { + let cred = { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_by_uuid(&conn, uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Credential not found: {}", uuid))? + }; + + match &cred.credential { + CredentialData::KiroOAuth { creds_file_path } => { + self.refresh_kiro_token(creds_file_path).await + } + CredentialData::GeminiOAuth { creds_file_path, .. } => { + self.refresh_gemini_token(creds_file_path).await + } + CredentialData::QwenOAuth { creds_file_path } => { + self.refresh_qwen_token(creds_file_path).await + } + _ => Err("此凭证类型不支持 Token 刷新".to_string()), + } + } + + /// 获取凭证池中指定凭证的 OAuth 状态 + pub fn get_credential_oauth_status( + &self, + db: &DbConnection, + uuid: &str, + ) -> Result { + let cred = { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_by_uuid(&conn, uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Credential not found: {}", uuid))? + }; + + let creds_path = get_oauth_creds_path(&cred.credential) + .ok_or_else(|| "此凭证类型不是 OAuth 凭证".to_string())?; + + self.get_oauth_status(&creds_path, &cred.provider_type.to_string()) + } +} diff --git a/src-tauri/src/services/token_cache_service.rs b/src-tauri/src/services/token_cache_service.rs new file mode 100644 index 000000000..b0e9d86dc --- /dev/null +++ b/src-tauri/src/services/token_cache_service.rs @@ -0,0 +1,433 @@ +//! Token 缓存管理服务 +//! +//! 负责管理凭证池中 OAuth Token 的生命周期: +//! - 从源文件加载初始 Token +//! - 缓存刷新后的 Token 到数据库 +//! - 按需刷新即将过期的 Token +//! - 处理 401/403 错误时的强制刷新 + +use crate::database::dao::provider_pool::ProviderPoolDao; +use crate::database::DbConnection; +use crate::models::provider_pool_model::{ + CachedTokenInfo, CredentialData, PoolProviderType, ProviderCredential, +}; +use crate::providers::gemini::GeminiProvider; +use crate::providers::kiro::KiroProvider; +use crate::providers::qwen::QwenProvider; +use chrono::Utc; +use dashmap::DashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// Token 缓存服务 +pub struct TokenCacheService { + /// 每凭证一把锁,防止并发刷新 + locks: DashMap>>, +} + +impl Default for TokenCacheService { + fn default() -> Self { + Self::new() + } +} + +impl TokenCacheService { + pub fn new() -> Self { + Self { + locks: DashMap::new(), + } + } + + /// 获取有效的 Token(核心方法) + /// + /// 1. 检查数据库缓存是否有效 + /// 2. 如果缓存有效且未过期,直接返回 + /// 3. 如果缓存无效或即将过期,执行刷新 + pub async fn get_valid_token( + &self, + db: &DbConnection, + uuid: &str, + ) -> Result { + // 首先检查缓存 + let cached = { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_token_cache(&conn, uuid).map_err(|e| e.to_string())? + }; + + // 缓存有效且未即将过期,直接返回 + if let Some(ref cache) = cached { + if cache.is_valid() && !cache.is_expiring_soon() { + if let Some(token) = &cache.access_token { + tracing::debug!( + "[TOKEN_CACHE] Using cached token for {}, expires at {:?}", + &uuid[..8], + cache.expiry_time + ); + return Ok(token.clone()); + } + } + } + + // 需要刷新(无缓存、已过期或即将过期) + self.refresh_and_cache(db, uuid, false).await + } + + /// 刷新 Token 并缓存到数据库 + /// + /// - force: 是否强制刷新(忽略缓存状态) + pub async fn refresh_and_cache( + &self, + db: &DbConnection, + uuid: &str, + force: bool, + ) -> Result { + // 获取该凭证的锁 + let lock = self + .locks + .entry(uuid.to_string()) + .or_insert_with(|| Arc::new(Mutex::new(()))) + .clone(); + + let _guard = lock.lock().await; + + // 双重检查:可能其他线程已完成刷新 + if !force { + let cached = { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_token_cache(&conn, uuid).map_err(|e| e.to_string())? + }; + + if let Some(cache) = cached { + if cache.is_valid() && !cache.is_expiring_soon() { + if let Some(token) = cache.access_token { + tracing::debug!( + "[TOKEN_CACHE] Double-check: another thread refreshed for {}", + &uuid[..8] + ); + return Ok(token); + } + } + } + } + + // 获取凭证信息 + let credential = { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_by_uuid(&conn, uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Credential not found: {}", uuid))? + }; + + tracing::info!( + "[TOKEN_CACHE] Refreshing token for {} ({})", + &uuid[..8], + credential.provider_type + ); + + // 执行刷新 + match self.do_refresh(&credential).await { + Ok(token_info) => { + // 缓存到数据库 + { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::update_token_cache(&conn, uuid, &token_info) + .map_err(|e| e.to_string())?; + } + + let token = token_info + .access_token + .ok_or_else(|| "Refresh succeeded but no access_token".to_string())?; + + tracing::info!( + "[TOKEN_CACHE] Token refreshed and cached for {}, expires at {:?}", + &uuid[..8], + token_info.expiry_time + ); + + Ok(token) + } + Err(e) => { + // 记录刷新错误 + { + let conn = db.lock().map_err(|e| e.to_string())?; + let _ = ProviderPoolDao::record_token_refresh_error(&conn, uuid, &e); + } + + tracing::error!("[TOKEN_CACHE] Token refresh failed for {}: {}", &uuid[..8], e); + + Err(e) + } + } + } + + /// 执行实际的 Token 刷新 + async fn do_refresh(&self, credential: &ProviderCredential) -> Result { + match &credential.credential { + CredentialData::KiroOAuth { creds_file_path } => { + self.refresh_kiro(creds_file_path).await + } + CredentialData::GeminiOAuth { + creds_file_path, .. + } => self.refresh_gemini(creds_file_path).await, + CredentialData::QwenOAuth { creds_file_path } => { + self.refresh_qwen(creds_file_path).await + } + CredentialData::OpenAIKey { api_key, .. } => { + // API Key 不需要刷新,直接返回 + Ok(CachedTokenInfo { + access_token: Some(api_key.clone()), + refresh_token: None, + expiry_time: None, // 永不过期 + last_refresh: Some(Utc::now()), + refresh_error_count: 0, + last_refresh_error: None, + }) + } + CredentialData::ClaudeKey { api_key, .. } => { + // API Key 不需要刷新,直接返回 + Ok(CachedTokenInfo { + access_token: Some(api_key.clone()), + refresh_token: None, + expiry_time: None, // 永不过期 + last_refresh: Some(Utc::now()), + refresh_error_count: 0, + last_refresh_error: None, + }) + } + } + } + + /// 刷新 Kiro Token + async fn refresh_kiro(&self, creds_path: &str) -> Result { + let mut provider = KiroProvider::new(); + provider + .load_credentials_from_path(creds_path) + .await + .map_err(|e| format!("加载 Kiro 凭证失败: {}", e))?; + + let token = provider + .refresh_token() + .await + .map_err(|e| format!("刷新 Kiro Token 失败: {}", e))?; + + // Kiro token 通常 1 小时过期,我们假设 50 分钟 + let expiry_time = Utc::now() + chrono::Duration::minutes(50); + + Ok(CachedTokenInfo { + access_token: Some(token), + refresh_token: provider.credentials.refresh_token.clone(), + expiry_time: Some(expiry_time), + last_refresh: Some(Utc::now()), + refresh_error_count: 0, + last_refresh_error: None, + }) + } + + /// 刷新 Gemini Token + async fn refresh_gemini(&self, creds_path: &str) -> Result { + let mut provider = GeminiProvider::new(); + provider + .load_credentials_from_path(creds_path) + .await + .map_err(|e| format!("加载 Gemini 凭证失败: {}", e))?; + + let token = provider + .refresh_token() + .await + .map_err(|e| format!("刷新 Gemini Token 失败: {}", e))?; + + // Gemini token 通常 1 小时过期 + let expiry_time = provider + .credentials + .expiry_date + .map(|ts| chrono::DateTime::from_timestamp(ts, 0).unwrap_or_default()) + .unwrap_or_else(|| Utc::now() + chrono::Duration::minutes(50)); + + Ok(CachedTokenInfo { + access_token: Some(token), + refresh_token: provider.credentials.refresh_token.clone(), + expiry_time: Some(expiry_time), + last_refresh: Some(Utc::now()), + refresh_error_count: 0, + last_refresh_error: None, + }) + } + + /// 刷新 Qwen Token + async fn refresh_qwen(&self, creds_path: &str) -> Result { + let mut provider = QwenProvider::new(); + provider + .load_credentials_from_path(creds_path) + .await + .map_err(|e| format!("加载 Qwen 凭证失败: {}", e))?; + + let token = provider + .refresh_token() + .await + .map_err(|e| format!("刷新 Qwen Token 失败: {}", e))?; + + // Qwen token 通常 1 小时过期 + let expiry_time = provider + .credentials + .expiry_date + .map(|ts| chrono::DateTime::from_timestamp(ts, 0).unwrap_or_default()) + .unwrap_or_else(|| Utc::now() + chrono::Duration::minutes(50)); + + Ok(CachedTokenInfo { + access_token: Some(token), + refresh_token: provider.credentials.refresh_token.clone(), + expiry_time: Some(expiry_time), + last_refresh: Some(Utc::now()), + refresh_error_count: 0, + last_refresh_error: None, + }) + } + + /// 从源文件加载初始 Token(首次使用时) + pub async fn load_initial_token( + &self, + db: &DbConnection, + uuid: &str, + ) -> Result { + let credential = { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_by_uuid(&conn, uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Credential not found: {}", uuid))? + }; + + // 尝试从源文件读取 token + let token_info = self.read_token_from_source(&credential).await?; + + // 缓存到数据库 + { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::update_token_cache(&conn, uuid, &token_info) + .map_err(|e| e.to_string())?; + } + + token_info + .access_token + .ok_or_else(|| "源文件中没有 access_token".to_string()) + } + + /// 从源文件读取 Token(不刷新) + async fn read_token_from_source( + &self, + credential: &ProviderCredential, + ) -> Result { + match &credential.credential { + CredentialData::KiroOAuth { creds_file_path } => { + let content = tokio::fs::read_to_string(creds_file_path) + .await + .map_err(|e| format!("读取 Kiro 凭证文件失败: {}", e))?; + let creds: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("解析凭证失败: {}", e))?; + + let access_token = creds["accessToken"] + .as_str() + .or_else(|| creds["access_token"].as_str()) + .map(|s| s.to_string()); + let refresh_token = creds["refreshToken"] + .as_str() + .or_else(|| creds["refresh_token"].as_str()) + .map(|s| s.to_string()); + + Ok(CachedTokenInfo { + access_token, + refresh_token, + expiry_time: None, // Kiro 源文件通常没有过期时间 + last_refresh: None, + refresh_error_count: 0, + last_refresh_error: None, + }) + } + CredentialData::GeminiOAuth { + creds_file_path, .. + } => { + let content = tokio::fs::read_to_string(creds_file_path) + .await + .map_err(|e| format!("读取 Gemini 凭证文件失败: {}", e))?; + let creds: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("解析凭证失败: {}", e))?; + + let access_token = creds["access_token"].as_str().map(|s| s.to_string()); + let refresh_token = creds["refresh_token"].as_str().map(|s| s.to_string()); + let expiry_time = creds["expiry_date"] + .as_i64() + .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)); + + Ok(CachedTokenInfo { + access_token, + refresh_token, + expiry_time, + last_refresh: None, + refresh_error_count: 0, + last_refresh_error: None, + }) + } + CredentialData::QwenOAuth { creds_file_path } => { + let content = tokio::fs::read_to_string(creds_file_path) + .await + .map_err(|e| format!("读取 Qwen 凭证文件失败: {}", e))?; + let creds: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("解析凭证失败: {}", e))?; + + let access_token = creds["access_token"].as_str().map(|s| s.to_string()); + let refresh_token = creds["refresh_token"].as_str().map(|s| s.to_string()); + let expiry_time = creds["expiry_date"] + .as_i64() + .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)); + + Ok(CachedTokenInfo { + access_token, + refresh_token, + expiry_time, + last_refresh: None, + refresh_error_count: 0, + last_refresh_error: None, + }) + } + CredentialData::OpenAIKey { api_key, .. } => Ok(CachedTokenInfo { + access_token: Some(api_key.clone()), + refresh_token: None, + expiry_time: None, + last_refresh: None, + refresh_error_count: 0, + last_refresh_error: None, + }), + CredentialData::ClaudeKey { api_key, .. } => Ok(CachedTokenInfo { + access_token: Some(api_key.clone()), + refresh_token: None, + expiry_time: None, + last_refresh: None, + refresh_error_count: 0, + last_refresh_error: None, + }), + } + } + + /// 清除凭证的 Token 缓存 + pub fn clear_cache(&self, db: &DbConnection, uuid: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::clear_token_cache(&conn, uuid).map_err(|e| e.to_string()) + } + + /// 检查凭证类型是否支持 Token 刷新 + pub fn supports_refresh(provider_type: PoolProviderType) -> bool { + matches!( + provider_type, + PoolProviderType::Kiro | PoolProviderType::Gemini | PoolProviderType::Qwen + ) + } + + /// 获取凭证的缓存状态 + pub fn get_cache_status( + &self, + db: &DbConnection, + uuid: &str, + ) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderPoolDao::get_token_cache(&conn, uuid).map_err(|e| e.to_string()) + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e2ef26244..d092963ed 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", - "version": "0.6.0", + "version": "0.6.1", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 8f2829342..a4f602f77 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,13 +7,12 @@ import { SwitchPage } from "./components/switch"; import { ClientsPage } from "./components/clients"; import { McpPage } from "./components/mcp"; import { PromptsPage } from "./components/prompts"; -import { CredentialsPage } from "./components/credentials/CredentialsPage"; import { ApiServerPage } from "./components/api-server/ApiServerPage"; import { SkillsPage } from "./components/skills"; +import { ProviderPoolPage } from "./components/provider-pool"; type Page = | "dashboard" - | "credentials" | "clients" | "api-server" | "providers" @@ -21,7 +20,8 @@ type Page = | "switch" | "mcp" | "prompts" - | "skills"; + | "skills" + | "provider-pool"; function App() { const [currentPage, setCurrentPage] = useState("dashboard"); @@ -30,8 +30,8 @@ function App() { switch (currentPage) { case "dashboard": return ; - case "credentials": - return ; + case "provider-pool": + return ; case "clients": return ; case "api-server": diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 924ac99e9..7b0a3fbaa 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -3,16 +3,15 @@ import { Settings, Plug, MessageSquare, - Key, Monitor, Globe, Boxes, + Database, } from "lucide-react"; import { cn } from "@/lib/utils"; type Page = | "dashboard" - | "credentials" | "clients" | "api-server" | "providers" @@ -20,7 +19,8 @@ type Page = | "switch" | "mcp" | "prompts" - | "skills"; + | "skills" + | "provider-pool"; interface SidebarProps { currentPage: Page; @@ -29,7 +29,7 @@ interface SidebarProps { const navItems = [ { id: "dashboard" as Page, label: "仪表盘", icon: LayoutDashboard }, - { id: "credentials" as Page, label: "凭证管理", icon: Key }, + { id: "provider-pool" as Page, label: "凭证池", icon: Database }, { id: "clients" as Page, label: "AI Clients", icon: Monitor }, { id: "api-server" as Page, label: "API Server", icon: Globe }, { id: "mcp" as Page, label: "MCP", icon: Plug }, diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 7c81b1b8f..67925fd7c 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -14,6 +14,7 @@ import { } from "lucide-react"; import { ModelsTab } from "./ModelsTab"; import { LogsTab } from "./LogsTab"; +import { RoutesTab } from "./RoutesTab"; import { startServer, stopServer, @@ -44,7 +45,7 @@ interface TestState { httpStatus?: number; } -type TabId = "server" | "openai" | "claude" | "models" | "logs"; +type TabId = "server" | "routes" | "openai" | "claude" | "models" | "logs"; export function ApiServerPage() { const [status, setStatus] = useState(null); @@ -453,6 +454,7 @@ export function ApiServerPage() {
{[ { id: "server" as TabId, name: "服务器控制" }, + { id: "routes" as TabId, name: "路由端点" }, { id: "openai" as TabId, name: "OpenAI 自定义" }, { id: "claude" as TabId, name: "Claude 自定义" }, { id: "models" as TabId, name: "可用模型" }, @@ -691,6 +693,9 @@ export function ApiServerPage() {
)} + {/* Routes Tab */} + {activeTab === "routes" && } + {/* OpenAI Custom Tab */} {activeTab === "openai" && (
diff --git a/src/components/api-server/RoutesTab.tsx b/src/components/api-server/RoutesTab.tsx new file mode 100644 index 000000000..1c3d3ad0b --- /dev/null +++ b/src/components/api-server/RoutesTab.tsx @@ -0,0 +1,300 @@ +import { useState, useEffect } from "react"; +import { Copy, Check, RefreshCw, Globe, Server, Tag } from "lucide-react"; +import { + routesApi, + RouteInfo, + RouteListResponse, + CurlExample, +} from "@/lib/api/routes"; + +export function RoutesTab() { + const [routes, setRoutes] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [expandedRoute, setExpandedRoute] = useState(null); + const [curlExamples, setCurlExamples] = useState< + Record + >({}); + const [copiedUrl, setCopiedUrl] = useState(null); + const [copiedCmd, setCopiedCmd] = useState(null); + + const fetchRoutes = async () => { + setLoading(true); + setError(null); + try { + const data = await routesApi.getAvailableRoutes(); + setRoutes(data); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } + setLoading(false); + }; + + useEffect(() => { + fetchRoutes(); + }, []); + + const fetchCurlExamples = async (selector: string) => { + if (curlExamples[selector]) return; + try { + const examples = await routesApi.getCurlExamples(selector); + setCurlExamples((prev) => ({ ...prev, [selector]: examples })); + } catch (e) { + console.error("Failed to fetch curl examples:", e); + } + }; + + const handleExpand = (selector: string) => { + if (expandedRoute === selector) { + setExpandedRoute(null); + } else { + setExpandedRoute(selector); + fetchCurlExamples(selector); + } + }; + + const copyToClipboard = (text: string, type: "url" | "cmd", id: string) => { + navigator.clipboard.writeText(text); + if (type === "url") { + setCopiedUrl(id); + setTimeout(() => setCopiedUrl(null), 2000); + } else { + setCopiedCmd(id); + setTimeout(() => setCopiedCmd(null), 2000); + } + }; + + const getProviderColor = (provider: string) => { + switch (provider) { + case "kiro": + return "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"; + case "gemini": + return "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"; + case "qwen": + return "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"; + case "openai": + return "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"; + case "claude": + return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"; + default: + return "bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400"; + } + }; + + if (loading && !routes) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

可用路由端点

+

+ 通过不同的 URL 路径访问不同的 Provider +

+
+ +
+ + {error && ( +
+ {error} +
+ )} + + {routes && ( +
+ {/* Base URL Info */} +
+
+ + 服务器地址: + + {routes.base_url} + +
+
+ + {/* Routes List */} +
+ {routes.routes.map((route) => ( + handleExpand(route.selector)} + curlExamples={curlExamples[route.selector]} + copiedUrl={copiedUrl} + copiedCmd={copiedCmd} + onCopyUrl={(url, id) => copyToClipboard(url, "url", id)} + onCopyCmd={(cmd, id) => copyToClipboard(cmd, "cmd", id)} + getProviderColor={getProviderColor} + /> + ))} +
+
+ )} +
+ ); +} + +interface RouteCardProps { + route: RouteInfo; + expanded: boolean; + onExpand: () => void; + curlExamples?: CurlExample[]; + copiedUrl: string | null; + copiedCmd: string | null; + onCopyUrl: (url: string, id: string) => void; + onCopyCmd: (cmd: string, id: string) => void; + getProviderColor: (provider: string) => string; +} + +function RouteCard({ + route, + expanded, + onExpand, + curlExamples, + copiedUrl, + copiedCmd, + onCopyUrl, + onCopyCmd, + getProviderColor, +}: RouteCardProps) { + return ( +
+ {/* Header */} +
+
+ +
+
+ {route.selector} + + {route.provider_type} + + {route.tags.map((tag) => ( + + + {tag} + + ))} +
+
+ {route.credential_count} 个凭证 + {!route.enabled && ( + (已禁用) + )} +
+
+
+
+ {expanded ? "收起" : "展开"} +
+
+ + {/* Expanded Content */} + {expanded && ( +
+ {/* Endpoints */} +
+

端点地址

+
+ {route.endpoints.map((endpoint, idx) => ( +
+
+ + {endpoint.protocol.toUpperCase()} + + {endpoint.url} +
+ +
+ ))} +
+
+ + {/* Curl Examples */} + {curlExamples && curlExamples.length > 0 && ( +
+

curl 示例

+
+ {curlExamples.map((example, idx) => ( +
+
+ + {example.description} + + +
+
+                      {example.command}
+                    
+
+ ))} +
+
+ )} +
+ )} +
+ ); +} diff --git a/src/components/credentials/CredentialsPage.tsx b/src/components/credentials/CredentialsPage.tsx deleted file mode 100644 index cc548690b..000000000 --- a/src/components/credentials/CredentialsPage.tsx +++ /dev/null @@ -1,411 +0,0 @@ -import { useState, useEffect } from "react"; -import { - RefreshCw, - FolderOpen, - AlertCircle, - CheckCircle2, - Eye, - EyeOff, - Copy, - FileText, - Key, -} from "lucide-react"; -import { - useOAuthCredentials, - useAllOAuthCredentials, -} from "@/hooks/useOAuthCredentials"; -import { OAuthProvider, EnvVariable } from "@/lib/api/credentials"; - -type TabId = "kiro" | "gemini" | "qwen"; - -const tabs: { id: TabId; name: string; description: string }[] = [ - { - id: "kiro", - name: "Kiro Claude", - description: "通过 Kiro OAuth 访问 Claude Sonnet 4.5", - }, - { - id: "gemini", - name: "Gemini CLI", - description: "通过 Gemini CLI OAuth 访问 Gemini 模型", - }, - { - id: "qwen", - name: "通义千问", - description: "通过 Qwen OAuth 访问通义千问", - }, -]; - -function CredentialPanel({ provider }: { provider: OAuthProvider }) { - const { - credentials, - envVariables, - loading, - refreshing, - error, - reloadFromFile, - refreshToken, - } = useOAuthCredentials(provider); - - const [showEnv, setShowEnv] = useState(false); - const [showValues, setShowValues] = useState(false); - const [copied, setCopied] = useState(null); - const [message, setMessage] = useState<{ - type: "success" | "error"; - text: string; - } | null>(null); - - const handleReload = async () => { - setMessage(null); - try { - await reloadFromFile(); - setMessage({ type: "success", text: "凭证加载成功!" }); - } catch (e) { - setMessage({ - type: "error", - text: `加载失败: ${e instanceof Error ? e.message : String(e)}`, - }); - } - }; - - const handleRefresh = async () => { - setMessage(null); - try { - await refreshToken(); - setMessage({ type: "success", text: "Token 刷新成功!" }); - } catch (e) { - setMessage({ - type: "error", - text: `刷新失败: ${e instanceof Error ? e.message : String(e)}`, - }); - } - }; - - const copyValue = (key: string, value: string) => { - navigator.clipboard.writeText(value); - setCopied(key); - setTimeout(() => setCopied(null), 2000); - }; - - const copyAllEnv = (vars: EnvVariable[]) => { - navigator.clipboard.writeText( - vars.map((v) => `${v.key}=${v.value}`).join("\n"), - ); - setCopied("all"); - setTimeout(() => setCopied(null), 2000); - }; - - const isLoading = loading || refreshing; - - // Provider-specific extra info - const renderExtraInfo = () => { - if (!credentials?.extra) return null; - const extra = credentials.extra as Record; - - if (provider === "kiro") { - return ( - <> -
- 区域: - {(extra.region as string) || "未设置"} -
-
- 认证方式: - - {(extra.auth_method as string) || "social"} - -
- - ); - } - - if (provider === "qwen" && extra.resource_url) { - return ( -
- Resource URL: - - {extra.resource_url as string} - -
- ); - } - - return null; - }; - - return ( -
- {message && ( -
- {message.type === "success" ? ( - - ) : ( - - )} - {message.text} -
- )} - - {error && ( -
- - {error} -
- )} - -
-
-

- - 凭证状态 -

-
- - - {credentials?.is_valid - ? "有效" - : credentials?.loaded - ? "已加载" - : "未加载"} - -
-
- -
-
- 凭证路径: - - {credentials?.creds_path || "未知"} - -
-
- Access Token: - - {credentials?.has_access_token ? "已加载" : "未加载"} - -
-
- Refresh Token: - - {credentials?.has_refresh_token ? "已加载" : "未加载"} - -
- {renderExtraInfo()} -
- -
- - - -
-
- - {/* Environment Variables */} - {showEnv && ( -
-
-

.env 环境变量

-
- - -
-
- {envVariables.length === 0 ? ( -

- 暂无环境变量,请先加载凭证 -

- ) : ( -
- {envVariables.map((v) => ( -
- - {v.key} - - = - - {showValues ? v.value : v.masked} - - -
- ))} -
- )} -
- )} -
- ); -} - -function CredentialOverview() { - const { credentials, reload } = useAllOAuthCredentials(); - - useEffect(() => { - reload(); - }, [reload]); - - return ( -
- {credentials.map((cred) => { - const tab = tabs.find((t) => t.id === cred.provider); - return ( -
-
-

{tab?.name || cred.provider}

- -
-

- {tab?.description || ""} -

-
-
- Access Token - - {cred.has_access_token ? "已加载" : "未加载"} - -
-
- Refresh Token - - {cred.has_refresh_token ? "已加载" : "未加载"} - -
-
-
- ); - })} -
- ); -} - -export function CredentialsPage() { - const [activeTab, setActiveTab] = useState("kiro"); - - return ( -
-
-

OAuth 凭证管理

-

- 管理 Kiro/Gemini/Qwen 的 OAuth 凭证 -

-
- - {/* Overview Cards */} - - - {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Tab Content */} - - -

- 系统每 5 秒自动检查凭证文件变化,如有更新会自动重新加载并记录日志 -

-
- ); -} diff --git a/src/components/provider-pool/AddCredentialModal.tsx b/src/components/provider-pool/AddCredentialModal.tsx new file mode 100644 index 000000000..37406e9c3 --- /dev/null +++ b/src/components/provider-pool/AddCredentialModal.tsx @@ -0,0 +1,270 @@ +import { useState } from "react"; +import { X, Key, FolderOpen } from "lucide-react"; +import { open } from "@tauri-apps/plugin-dialog"; +import { providerPoolApi, PoolProviderType } from "@/lib/api/providerPool"; + +interface AddCredentialModalProps { + providerType: PoolProviderType; + onClose: () => void; + onSuccess: () => void; +} + +// Default credential paths +const defaultCredsPath: Record = { + kiro: "~/.aws/sso/cache/kiro-auth-token.json", + gemini: "~/.gemini/oauth_creds.json", + qwen: "~/.qwen/oauth_creds.json", +}; + +export function AddCredentialModal({ + providerType, + onClose, + onSuccess, +}: AddCredentialModalProps) { + const [name, setName] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // OAuth fields - initialize with default path + const [credsFilePath, setCredsFilePath] = useState( + defaultCredsPath[providerType] || "", + ); + const [projectId, setProjectId] = useState(""); + + // API Key fields + const [apiKey, setApiKey] = useState(""); + const [baseUrl, setBaseUrl] = useState(""); + + const isOAuth = ["kiro", "gemini", "qwen"].includes(providerType); + + const providerLabels: Record = { + kiro: "Kiro (AWS)", + gemini: "Gemini (Google)", + qwen: "Qwen (阿里)", + openai: "OpenAI", + claude: "Claude (Anthropic)", + }; + + const handleSelectFile = async () => { + try { + const selected = await open({ + multiple: false, + filters: [{ name: "JSON", extensions: ["json"] }], + }); + if (selected) { + setCredsFilePath(selected as string); + } + } catch (e) { + console.error("Failed to open file dialog:", e); + } + }; + + const handleSubmit = async () => { + setLoading(true); + setError(null); + + try { + const trimmedName = name.trim() || undefined; + + if (isOAuth) { + if (!credsFilePath) { + setError("请选择凭证文件"); + return; + } + + switch (providerType) { + case "kiro": + await providerPoolApi.addKiroOAuth(credsFilePath, trimmedName); + break; + case "gemini": + await providerPoolApi.addGeminiOAuth( + credsFilePath, + projectId.trim() || undefined, + trimmedName, + ); + break; + case "qwen": + await providerPoolApi.addQwenOAuth(credsFilePath, trimmedName); + break; + } + } else { + if (!apiKey) { + setError("请输入 API Key"); + return; + } + + switch (providerType) { + case "openai": + await providerPoolApi.addOpenAIKey( + apiKey, + baseUrl.trim() || undefined, + trimmedName, + ); + break; + case "claude": + await providerPoolApi.addClaudeKey( + apiKey, + baseUrl.trim() || undefined, + trimmedName, + ); + break; + } + } + + onSuccess(); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Header */} +
+

+ 添加 {providerLabels[providerType]} 凭证 +

+ +
+ + {/* Content */} +
+ {/* Name field */} +
+ + setName(e.target.value)} + placeholder="给这个凭证起个名字..." + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +
+ + {isOAuth ? ( + <> + {/* Credential File */} +
+ +
+ setCredsFilePath(e.target.value)} + placeholder="输入凭证文件的完整路径..." + className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm" + /> + +
+

+ {providerType === "kiro" && + "默认路径: ~/.aws/sso/cache/kiro-auth-token.json"} + {providerType === "gemini" && + "默认路径: ~/.gemini/oauth_creds.json"} + {providerType === "qwen" && + "默认路径: ~/.qwen/oauth_creds.json"} +

+
+ + {/* Gemini specific: Project ID */} + {providerType === "gemini" && ( +
+ + setProjectId(e.target.value)} + placeholder="Google Cloud Project ID..." + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +
+ )} + + ) : ( + <> + {/* API Key */} +
+ +
+ + setApiKey(e.target.value)} + placeholder="sk-..." + className="w-full rounded-lg border bg-background pl-10 pr-3 py-2 text-sm" + /> +
+
+ + {/* Base URL */} +
+ + setBaseUrl(e.target.value)} + placeholder={ + providerType === "openai" + ? "https://api.openai.com/v1" + : "https://api.anthropic.com/v1" + } + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +

+ 留空使用默认 URL,或输入自定义代理地址 +

+
+ + )} + + {/* Error */} + {error && ( +
+ {error} +
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/src/components/provider-pool/CredentialCard.tsx b/src/components/provider-pool/CredentialCard.tsx new file mode 100644 index 000000000..e447a1c50 --- /dev/null +++ b/src/components/provider-pool/CredentialCard.tsx @@ -0,0 +1,401 @@ +import { + Heart, + HeartOff, + Trash2, + RotateCcw, + Activity, + Power, + PowerOff, + Clock, + AlertTriangle, + RefreshCw, + Key, + CheckCircle, + XCircle, + Database, + Settings, +} from "lucide-react"; +import type { CredentialDisplay } from "@/lib/api/providerPool"; + +interface CredentialCardProps { + credential: CredentialDisplay; + onToggle: () => void; + onDelete: () => void; + onReset: () => void; + onCheckHealth: () => void; + onRefreshToken?: () => void; + onEdit: () => void; + deleting: boolean; + checkingHealth: boolean; + refreshingToken?: boolean; +} + +export function CredentialCard({ + credential, + onToggle, + onDelete, + onReset, + onCheckHealth, + onRefreshToken, + onEdit, + deleting, + checkingHealth, + refreshingToken, +}: CredentialCardProps) { + const formatDate = (dateStr?: string) => { + if (!dateStr) return "从未"; + const date = new Date(dateStr); + return date.toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + }; + + const getCredentialTypeLabel = (type: string) => { + const labels: Record = { + kiro_oauth: "OAuth", + gemini_oauth: "OAuth", + qwen_oauth: "OAuth", + openai_key: "API Key", + claude_key: "API Key", + }; + return labels[type] || type; + }; + + const isHealthy = credential.is_healthy && !credential.is_disabled; + const hasError = credential.error_count > 0; + const isOAuth = credential.credential_type.includes("oauth"); + + return ( +
+ {/* Header */} +
+
+
+

+ {credential.name || `凭证 #${credential.uuid.slice(0, 12)}`} +

+ + {getCredentialTypeLabel(credential.credential_type)} + +
+

+ {credential.uuid.slice(0, 24)}... +

+
+
+
+ {credential.is_disabled ? ( + + ) : isHealthy ? ( + + ) : ( + + )} +
+
+
+ + {/* Stats */} +
+
+
+
+ +
+
+
使用次数
+
+ {credential.usage_count} +
+
+
+
+
+ +
+
+
错误次数
+
+ {credential.error_count} +
+
+
+
+ + + 最后使用: {formatDate(credential.last_used)} + +
+
+
+ + {/* Health Check Info */} + {credential.last_health_check_time && ( +
+ + 检查: {formatDate(credential.last_health_check_time)} + {credential.last_health_check_model && + ` (${credential.last_health_check_model})`} + +
+ )} + + {/* OAuth Status */} + {isOAuth && credential.oauth_status && ( +
+
+
+ +
+ OAuth 状态 +
+
+
+ + Access Token + +
+ {credential.oauth_status.has_access_token ? ( + + ) : ( + + )} + + {credential.oauth_status.has_access_token ? "有效" : "缺失"} + +
+
+
+ Token 状态 +
+ {credential.oauth_status.is_token_valid ? ( + + ) : ( + + )} + + {credential.oauth_status.is_token_valid ? "有效" : "需刷新"} + +
+
+ {credential.oauth_status.expiry_info && ( +
+ + 过期时间: {credential.oauth_status.expiry_info} + +
+ )} +
+
+ )} + + {/* Token Cache Status */} + {isOAuth && credential.token_cache_status && ( +
+
+
+ +
+ Token 缓存 +
+
+
+ 缓存状态 +
+ {credential.token_cache_status.has_cached_token ? ( + + ) : ( + + )} + + {credential.token_cache_status.has_cached_token + ? "已缓存" + : "未缓存"} + +
+
+
+ 有效性 +
+ {credential.token_cache_status.is_valid ? ( + credential.token_cache_status.is_expiring_soon ? ( + + ) : ( + + ) + ) : ( + + )} + + {credential.token_cache_status.is_valid + ? credential.token_cache_status.is_expiring_soon + ? "即将过期" + : "有效" + : "已过期"} + +
+
+ {(credential.token_cache_status.last_refresh || + credential.token_cache_status.expiry_time) && ( +
+ {credential.token_cache_status.last_refresh && ( +
+ 最后刷新:{" "} + {formatDate(credential.token_cache_status.last_refresh)} +
+ )} + {credential.token_cache_status.expiry_time && ( +
+ 过期时间:{" "} + {formatDate(credential.token_cache_status.expiry_time)} +
+ )} +
+ )} + {credential.token_cache_status.refresh_error_count > 0 && ( +
+
+ + + 刷新失败 {credential.token_cache_status.refresh_error_count}{" "} + 次 + +
+ {credential.token_cache_status.last_refresh_error && ( +
+ {credential.token_cache_status.last_refresh_error.slice( + 0, + 60, + )} + {credential.token_cache_status.last_refresh_error.length > + 60 && "..."} +
+ )} +
+ )} +
+
+ )} + + {/* Error Message */} + {credential.last_error_message && ( +
+ {credential.last_error_message.slice(0, 100)} + {credential.last_error_message.length > 100 && "..."} +
+ )} + + {/* Actions */} +
+ + + + +
+ + + {isOAuth && onRefreshToken && ( + + )} + + + + +
+
+
+ ); +} diff --git a/src/components/provider-pool/EditCredentialModal.tsx b/src/components/provider-pool/EditCredentialModal.tsx new file mode 100644 index 000000000..b562e3e86 --- /dev/null +++ b/src/components/provider-pool/EditCredentialModal.tsx @@ -0,0 +1,349 @@ +import { useState, useEffect } from "react"; +import { X, Eye, EyeOff, Settings, FolderOpen, Upload, CheckCircle } from "lucide-react"; +import { open } from "@tauri-apps/plugin-dialog"; +import { + CredentialDisplay, + UpdateCredentialRequest, +} from "@/lib/api/providerPool"; + +interface EditCredentialModalProps { + credential: CredentialDisplay | null; + isOpen: boolean; + onClose: () => void; + onEdit: (uuid: string, request: UpdateCredentialRequest) => Promise; +} + +export function EditCredentialModal({ + credential, + isOpen, + onClose, + onEdit, +}: EditCredentialModalProps) { + const [name, setName] = useState(""); + const [checkHealth, setCheckHealth] = useState(true); + const [checkModelName, setCheckModelName] = useState(""); + const [notSupportedModelsText, setNotSupportedModelsText] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showCredentialDetails, setShowCredentialDetails] = useState(false); + + // 重新上传文件相关状态 + const [newCredFilePath, setNewCredFilePath] = useState(""); + const [newProjectId, setNewProjectId] = useState(""); + + // 初始化表单数据 + useEffect(() => { + if (credential) { + setName(credential.name || ""); + setCheckHealth(credential.check_health); + setCheckModelName(credential.check_model_name || ""); + setNotSupportedModelsText( + (credential.not_supported_models || []).join(", "), + ); + setNewCredFilePath(""); + setNewProjectId(""); + setError(null); + } + }, [credential]); + + if (!isOpen || !credential) { + return null; + } + + const isOAuth = credential.credential_type.includes("oauth"); + + const handleSelectNewFile = async () => { + try { + const selected = await open({ + multiple: false, + filters: [{ name: "JSON", extensions: ["json"] }], + }); + if (selected) { + setNewCredFilePath(selected as string); + } + } catch (e) { + console.error("Failed to open file dialog:", e); + } + }; + + const getMaskedCredentialInfo = () => { + if (isOAuth) { + // OAuth 凭证显示文件路径(部分遮罩) + const path = credential.display_credential; + const parts = path.split("/"); + if (parts.length > 1) { + const fileName = parts[parts.length - 1]; + const dirPath = parts.slice(0, -1).join("/"); + return `${dirPath}/***${fileName.slice(-8)}`; + } + return `***${path.slice(-12)}`; + } else { + // API Key 显示遮罩 + return credential.display_credential; + } + }; + + const handleSubmit = async () => { + setLoading(true); + setError(null); + + try { + // 解析不支持的模型列表 + const parsedNotSupportedModels = notSupportedModelsText + .split(",") + .map((model) => model.trim()) + .filter((model) => model.length > 0); + + const updateRequest: UpdateCredentialRequest = { + name: name.trim() || undefined, + check_health: checkHealth, + check_model_name: checkModelName.trim() || undefined, + not_supported_models: + parsedNotSupportedModels.length > 0 + ? parsedNotSupportedModels + : undefined, + new_creds_file_path: newCredFilePath.trim() || undefined, + new_project_id: newProjectId.trim() || undefined, + }; + + await onEdit(credential.uuid, updateRequest); + onClose(); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Header */} +
+

+ + 编辑凭证 +

+ +
+ + {/* Content - Scrollable */} +
+
+ {/* 凭证信息(只读) */} +
+
+ + +
+
+
+ 类型: + {credential.credential_type} +
+
+ UUID: + + {credential.uuid.slice(0, 24)}... + +
+
+ + {isOAuth ? "文件路径:" : "API Key:"} + + + {showCredentialDetails + ? credential.display_credential + : getMaskedCredentialInfo()} + +
+
+

+ 🔒 敏感信息(API Key、文件路径)无法修改,如需更改请删除后重新添加 +

+
+ + {/* 可编辑字段 */} +
+ + setName(e.target.value)} + placeholder="给这个凭证起个名字..." + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +
+ + {/* 健康检查设置 */} +
+ + {checkHealth && ( +
+ + setCheckModelName(e.target.value)} + placeholder="留空使用默认模型..." + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +
+ )} +
+ + {/* 不支持的模型列表 */} +
+ +