From d29efe737ced50cf460335cb68355ba0dab92d48 Mon Sep 17 00:00:00 2001 From: coso Date: Tue, 6 Jan 2026 22:25:46 +0800 Subject: [PATCH] chore: bump version to v0.32.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- package.json | 2 +- src-tauri/Cargo.lock | 521 ++++++---- src-tauri/Cargo.toml | 2 +- src-tauri/src/agent/native_agent.rs | 127 ++- src-tauri/src/agent/protocols/openai.rs | 88 ++ src-tauri/src/agent/types.rs | 63 +- src-tauri/src/app/commands/api_test.rs | 93 +- src-tauri/src/app/commands/config.rs | 16 +- src-tauri/src/app/runner.rs | 5 - src-tauri/src/commands/agent_cmd.rs | 11 +- src-tauri/src/commands/model_registry_cmd.rs | 2 +- src-tauri/src/commands/native_agent_cmd.rs | 55 +- src-tauri/src/commands/oauth_plugin_cmd.rs | 15 +- src-tauri/src/commands/router_cmd.rs | 440 -------- src-tauri/src/config/mod.rs | 6 +- src-tauri/src/config/tests.rs | 3 + src-tauri/src/config/types.rs | 97 ++ src-tauri/src/credential/registry.rs | 78 +- src-tauri/src/data/local_models.rs | 984 ------------------ src-tauri/src/data/mod.rs | 7 +- src-tauri/src/server/handlers/api.rs | 191 ++-- src-tauri/src/server/mod.rs | 22 +- .../src/services/api_key_provider_service.rs | 101 +- .../src/services/model_registry_service.rs | 276 +++-- .../src/services/provider_pool_service.rs | 61 +- src-tauri/tauri.conf.json | 2 +- .../agent/chat/components/ChatNavbar.tsx | 162 ++- src/components/api-server/ApiServerPage.tsx | 33 +- src/components/api-server/ModelsTab.tsx | 195 ++-- src/components/routing/ModelMapping.tsx | 171 --- src/components/routing/RoutingPage.tsx | 198 +--- src/components/routing/index.ts | 1 - src/lib/api/router.ts | 49 - src/lib/plugin-components/global.ts | 13 + src/lib/plugin-components/index.ts | 2 + src/lib/plugin-loader/index.ts | 15 + 36 files changed, 1589 insertions(+), 2518 deletions(-) delete mode 100644 src-tauri/src/data/local_models.rs delete mode 100644 src/components/routing/ModelMapping.tsx diff --git a/package.json b/package.json index 88f9b1bce..760bc4161 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.31.0", + "version": "0.32.0", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a2d090b58..96a174fa7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -92,9 +92,12 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "ashpd" @@ -210,7 +213,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -250,7 +253,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -267,7 +270,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -432,9 +435,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" [[package]] name = "bit-set" @@ -552,9 +555,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -667,14 +670,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -756,9 +759,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b042e5d8a74ae91bb0961acd039822472ec99f8ab0948cbf6d1369588f8be586" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] @@ -976,7 +979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -986,7 +989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1010,7 +1013,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1021,7 +1024,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1063,7 +1066,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1165,7 +1168,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1197,7 +1200,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1226,9 +1229,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -1260,7 +1263,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", "vswhom", "winreg 0.55.0", ] @@ -1304,7 +1307,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1390,6 +1393,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -1423,9 +1446,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flate2" @@ -1470,7 +1493,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1496,9 +1519,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824f08d01d0f496b3eca4f001a13cf17690a6ee930043d20817f547455fd98f8" +checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7" dependencies = [ "autocfg", "tokio", @@ -1608,7 +1631,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1869,7 +1892,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1948,14 +1971,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1970,6 +1993,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2168,7 +2202,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry", + "windows-registry 0.6.1", ] [[package]] @@ -2183,7 +2217,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.56.0", + "windows-core 0.62.2", ] [[package]] @@ -2202,14 +2236,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -2226,7 +2260,6 @@ checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", - "serde", "tinystr", "writeable", "zerovec", @@ -2234,11 +2267,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b24a59706036ba941c9476a55cd57b82b77f38a3c667d637ee7cabbc85eaedc" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -2249,31 +2281,29 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.2" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a97b8ac6235e69506e8dacfb2adf38461d2ce6d3e9bd9c94c4cbc3cd4400a4" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -2283,8 +2313,6 @@ checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "serde", - "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -2321,14 +2349,15 @@ dependencies = [ [[package]] name = "image" -version = "0.25.6" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", + "moxcms", "num-traits", - "png", + "png 0.18.0", "tiff", ] @@ -2401,9 +2430,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -2430,9 +2459,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "javascriptcore-rs" @@ -2489,12 +2518,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" - [[package]] name = "js-sys" version = "0.3.83" @@ -2602,9 +2625,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libloading" @@ -2628,13 +2651,13 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall", + "redox_syscall 0.7.0", ] [[package]] @@ -2712,7 +2735,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -2787,6 +2810,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.17.1" @@ -2802,7 +2835,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.17", "windows-sys 0.60.2", @@ -2938,10 +2971,10 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -3206,7 +3239,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -3312,7 +3345,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -3456,7 +3489,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -3523,7 +3556,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.12.1", - "quick-xml 0.38.4", + "quick-xml", "serde", "time", ] @@ -3541,6 +3574,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3611,7 +3657,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.9", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -3646,9 +3692,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -3674,7 +3720,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.31.0" +version = "0.32.0" dependencies = [ "anyhow", "arboard", @@ -3738,6 +3784,15 @@ dependencies = [ "zip", ] +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3745,13 +3800,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] -name = "quick-xml" -version = "0.37.5" +name = "quick-error" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" @@ -3764,9 +3816,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -3911,6 +3963,15 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -3950,7 +4011,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -3984,9 +4045,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.26" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -4105,9 +4166,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -4118,9 +4179,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", "once_cell", @@ -4141,9 +4202,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "zeroize", ] @@ -4173,16 +4234,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", - "quick-error", + "quick-error 1.2.3", "tempfile", "wait-timeout", ] [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -4231,9 +4292,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -4250,7 +4311,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -4355,7 +4416,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -4366,20 +4427,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -4401,7 +4462,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -4415,9 +4476,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -4446,7 +4507,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -4462,7 +4523,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -4497,7 +4558,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -4581,10 +4642,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -4643,7 +4705,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -4749,9 +4811,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" dependencies = [ "proc-macro2", "quote", @@ -4775,7 +4837,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -4860,7 +4922,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -4950,7 +5012,7 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", "walkdir", ] @@ -4965,14 +5027,14 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", "serde", "serde_json", "sha2", - "syn 2.0.111", + "syn 2.0.113", "tauri-utils", "thiserror 2.0.17", "time", @@ -4990,7 +5052,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "tauri-codegen", "tauri-utils", ] @@ -5008,7 +5070,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", "walkdir", ] @@ -5043,7 +5105,7 @@ dependencies = [ "thiserror 2.0.17", "tracing", "url", - "windows-registry", + "windows-registry 0.5.3", "windows-result 0.3.4", ] @@ -5083,7 +5145,7 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.17", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", "url", ] @@ -5206,7 +5268,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.17", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", "url", "urlpattern", "uuid", @@ -5221,14 +5283,14 @@ checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ "dunce", "embed-resource", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", ] [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -5274,7 +5336,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -5285,7 +5347,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -5299,13 +5361,16 @@ dependencies = [ [[package]] name = "tiff" -version = "0.9.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ + "fax", "flate2", - "jpeg-decoder", + "half", + "quick-error 2.0.1", "weezl", + "zune-jpeg", ] [[package]] @@ -5371,15 +5436,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", - "serde_core", "zerovec", ] [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -5401,7 +5465,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -5438,9 +5502,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5463,14 +5527,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ "indexmap 2.12.1", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.14", @@ -5487,9 +5551,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -5520,30 +5584,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.12.1", - "toml_datetime 0.7.3", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow 0.7.14", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow 0.7.14", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" @@ -5620,9 +5684,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -5638,14 +5702,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -5678,9 +5742,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", "dirs 6.0.0", @@ -5692,7 +5756,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.17", "windows-sys 0.60.2", @@ -5818,14 +5882,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -6008,7 +6073,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "wasm-bindgen-shared", ] @@ -6036,9 +6101,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", @@ -6050,9 +6115,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ "bitflags 2.10.0", "rustix", @@ -6062,9 +6127,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -6074,20 +6139,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" dependencies = [ "dlib", "log", @@ -6170,7 +6235,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6290,7 +6355,20 @@ dependencies = [ "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -6312,7 +6390,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6323,7 +6401,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6334,7 +6412,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6345,7 +6423,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6378,7 +6456,18 @@ checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -6399,6 +6488,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -6408,6 +6506,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -6894,7 +7001,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "synstructure", ] @@ -6942,7 +7049,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "zbus_names", "zvariant", "zvariant_utils", @@ -6977,7 +7084,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -6997,7 +7104,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "synstructure", ] @@ -7024,7 +7131,6 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ - "serde", "yoke", "zerofrom", "zerovec-derive", @@ -7038,7 +7144,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -7061,6 +7167,12 @@ dependencies = [ "zstd", ] +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" @@ -7090,6 +7202,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.8.0" @@ -7114,7 +7241,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "zvariant_utils", ] @@ -7127,6 +7254,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.111", + "syn 2.0.113", "winnow 0.7.14", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 82053dd02..baa266f6b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.31.0" +version = "0.32.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/agent/native_agent.rs b/src-tauri/src/agent/native_agent.rs index 07f12310a..687f39e64 100644 --- a/src-tauri/src/agent/native_agent.rs +++ b/src-tauri/src/agent/native_agent.rs @@ -44,6 +44,8 @@ pub struct NativeAgent { provider_type: ProviderType, /// 协议处理器 protocol: Box, + /// Provider ID,用于自定义 Provider 路由(如 "moonshot") + provider_id: Option, } impl NativeAgent { @@ -51,6 +53,7 @@ impl NativeAgent { base_url: String, api_key: String, provider_type: ProviderType, + provider_id: Option, ) -> Result { let client = Client::builder() .timeout(Duration::from_secs(300)) @@ -61,21 +64,27 @@ impl NativeAgent { let protocol = create_protocol(provider_type); + // 保存原始 base_url,provider_id 将在构建请求 URL 时使用 + let effective_base_url = base_url.clone(); + info!( - "[NativeAgent] 创建 Agent: base_url={}, provider={:?}, protocol_endpoint={}", + "[NativeAgent] 创建 Agent: base_url={}, effective_base_url={}, provider={:?}, provider_id={:?}, protocol_endpoint={}", base_url, + effective_base_url, provider_type, + provider_id, protocol.endpoint() ); Ok(Self { client, - base_url, + base_url: effective_base_url, api_key, sessions: Arc::new(RwLock::new(HashMap::new())), config: AgentConfig::default(), provider_type, protocol, + provider_id, }) } @@ -89,6 +98,57 @@ impl NativeAgent { self } + /// 获取 API 请求的有效 base_url + /// + /// 对于自定义 Provider(如 moonshot),返回 `{base_url}/api/provider/{provider_id}` + /// 对于内置 Provider,返回原始 base_url + fn get_effective_base_url(&self) -> String { + if let Some(ref pid) = self.provider_id { + // 检查 provider_id 是否是已知的内置类型 + let is_builtin = matches!( + pid.to_lowercase().as_str(), + "openai" + | "claude" + | "anthropic" + | "gemini" + | "kiro" + | "qwen" + | "codex" + | "antigravity" + | "iflow" + ); + if is_builtin { + self.base_url.clone() + } else { + // 自定义 Provider,使用 provider 特定路由 + // 例如:http://127.0.0.1:8999/api/provider/moonshot + format!("{}/api/provider/{}", self.base_url, pid) + } + } else { + self.base_url.clone() + } + } + + /// 检查是否是自定义 Provider + fn is_custom_provider(&self) -> bool { + if let Some(ref pid) = self.provider_id { + !matches!( + pid.to_lowercase().as_str(), + "openai" + | "claude" + | "anthropic" + | "gemini" + | "kiro" + | "qwen" + | "codex" + | "antigravity" + | "iflow" + ) + } else { + false + } + } + /// 发送聊天请求(非流式,用于简单场景) pub async fn chat(&self, request: NativeChatRequest) -> Result { let model = request.model.unwrap_or_else(|| self.config.model.clone()); @@ -126,7 +186,12 @@ impl NativeAgent { reasoning_effort: None, }; - let url = format!("{}/v1/chat/completions", self.base_url); + // 对于自定义 Provider,使用 provider 特定路由 + let url = if self.is_custom_provider() { + format!("{}/chat/completions", self.get_effective_base_url()) + } else { + format!("{}/v1/chat/completions", self.base_url) + }; let response = self .client @@ -241,11 +306,13 @@ impl NativeAgent { }; // 使用协议策略发送请求 + // 对于自定义 Provider,使用 provider 特定路由 + let effective_base_url = self.get_effective_base_url(); let result = self .protocol .chat_stream( &self.client, - &self.base_url, + &effective_base_url, &self.api_key, &history, &request.message, @@ -408,11 +475,13 @@ impl NativeAgent { }; // 使用协议策略继续对话 + // 对于自定义 Provider,使用 provider 特定路由 + let effective_base_url = self.get_effective_base_url(); let result = self .protocol .chat_stream_continue( &self.client, - &self.base_url, + &effective_base_url, &self.api_key, &session.messages, &model, @@ -676,8 +745,45 @@ impl NativeAgentState { base_url: String, api_key: String, provider_type: ProviderType, + provider_id: Option, ) -> Result<(), String> { - let agent = NativeAgent::new(base_url, api_key, provider_type)?; + let agent = NativeAgent::new(base_url, api_key, provider_type, provider_id)?; + *self.agent.write() = Some(agent); + Ok(()) + } + + /// 使用配置初始化 Agent + /// + /// 从 NativeAgentConfig 加载系统提示词等配置 + pub fn init_with_config( + &self, + base_url: String, + api_key: String, + provider_type: ProviderType, + provider_id: Option, + agent_config: &crate::config::NativeAgentConfig, + ) -> Result<(), String> { + let mut agent = NativeAgent::new(base_url, api_key, provider_type, provider_id)?; + + // 从配置加载系统提示词 + let system_prompt = agent_config.get_effective_system_prompt().or_else(|| { + // 如果配置启用了默认提示词,使用内置默认值 + if agent_config.use_default_system_prompt { + Some(super::types::DEFAULT_SYSTEM_PROMPT.to_string()) + } else { + None + } + }); + + if let Some(prompt) = system_prompt { + agent.config.system_prompt = Some(prompt); + } + + // 从配置加载其他参数 + agent.config.model = agent_config.default_model.clone(); + agent.config.temperature = Some(agent_config.temperature); + agent.config.max_tokens = Some(agent_config.max_tokens); + *self.agent.write() = Some(agent); Ok(()) } @@ -691,6 +797,14 @@ impl NativeAgentState { self.agent.read().as_ref().map(|a| a.provider_type) } + /// 获取当前 Agent 的 provider ID + pub fn get_provider_id(&self) -> Option { + self.agent + .read() + .as_ref() + .and_then(|a| a.provider_id.clone()) + } + pub fn reset(&self) { *self.agent.write() = None; } @@ -724,6 +838,7 @@ impl NativeAgentState { config: agent.config.clone(), provider_type: agent.provider_type, protocol, + provider_id: agent.provider_id.clone(), }) } diff --git a/src-tauri/src/agent/protocols/openai.rs b/src-tauri/src/agent/protocols/openai.rs index 4193fc90c..1d0499ef1 100644 --- a/src-tauri/src/agent/protocols/openai.rs +++ b/src-tauri/src/agent/protocols/openai.rs @@ -176,6 +176,52 @@ impl OpenAIProtocol { ); buffer.push_str(&text); + // 检查是否是非流式响应(直接返回完整 JSON) + // 非流式响应以 { 开头,不是 SSE 格式 + if buffer.trim().starts_with('{') && !buffer.contains("data: ") { + // 尝试解析为完整的 ChatCompletionResponse + if let Ok(response) = serde_json::from_str::< + crate::models::openai::ChatCompletionResponse, + >(&buffer) + { + eprintln!("[OpenAIProtocol] 检测到非流式响应,直接解析"); + + let content = response + .choices + .first() + .and_then(|c| c.message.content.clone()) + .unwrap_or_default(); + + // 发送完整内容作为 TextDelta + if !content.is_empty() { + let _ = tx + .send(StreamEvent::TextDelta { + text: content.clone(), + }) + .await; + } + + let usage = Some(crate::agent::types::TokenUsage { + input_tokens: response.usage.prompt_tokens, + output_tokens: response.usage.completion_tokens, + }); + + if send_done { + let _ = tx + .send(StreamEvent::Done { + usage: usage.clone(), + }) + .await; + } + + return Ok(StreamResult { + content, + tool_calls: None, + usage, + }); + } + } + // 处理完整的 SSE 事件(以 \n\n 分隔) while let Some(pos) = buffer.find("\n\n") { let event = buffer[..pos].to_string(); @@ -233,6 +279,48 @@ impl OpenAIProtocol { } // 流正常结束但没有收到 [DONE] + // 检查 buffer 中是否还有未处理的非流式响应 + if !buffer.trim().is_empty() && buffer.trim().starts_with('{') { + if let Ok(response) = + serde_json::from_str::(&buffer) + { + eprintln!("[OpenAIProtocol] 流结束时检测到非流式响应"); + + let content = response + .choices + .first() + .and_then(|c| c.message.content.clone()) + .unwrap_or_default(); + + if !content.is_empty() { + let _ = tx + .send(StreamEvent::TextDelta { + text: content.clone(), + }) + .await; + } + + let usage = Some(crate::agent::types::TokenUsage { + input_tokens: response.usage.prompt_tokens, + output_tokens: response.usage.completion_tokens, + }); + + if send_done { + let _ = tx + .send(StreamEvent::Done { + usage: usage.clone(), + }) + .await; + } + + return Ok(StreamResult { + content, + tool_calls: None, + usage, + }); + } + } + let full_content = parser.get_full_content(); let tool_calls = if parser.has_tool_calls() { Some(parser.finalize_tool_calls()) diff --git a/src-tauri/src/agent/types.rs b/src-tauri/src/agent/types.rs index d17f8e2fc..d6f63725d 100644 --- a/src-tauri/src/agent/types.rs +++ b/src-tauri/src/agent/types.rs @@ -213,7 +213,7 @@ impl Default for AgentConfig { fn default() -> Self { Self { model: "claude-sonnet-4-20250514".to_string(), - system_prompt: None, + system_prompt: Some(DEFAULT_SYSTEM_PROMPT.to_string()), temperature: Some(0.7), max_tokens: Some(4096), tools: Vec::new(), @@ -221,6 +221,67 @@ impl Default for AgentConfig { } } +/// 默认系统提示词 +/// +/// 参考 Manus Agent 的模块化设计,使用结构化的提示词组织 +/// 支持通过配置文件覆盖 +pub const DEFAULT_SYSTEM_PROMPT: &str = r#"你是 ProxyCast 内置的 AI 助手。 + + +- 你是一个友好、专业的 AI 助手 +- 擅长编程、文件操作和系统任务 +- 使用中文与用户交流 + + + +1. **自然交流优先**:问候、闲聊、问答类对话,直接用文字回复 +2. **显式授权操作**:只有当用户明确提供路径或命令时,才能执行工具 +3. **不主动探索**:不要未经请求就读取文件或执行命令 + + + +## 何时使用工具 + +✅ **使用工具的情况**: +- 用户明确提供了文件路径(如 "读取 /path/to/file") +- 用户明确要求执行命令(如 "运行 npm install") +- 用户要求创建或修改文件 + +❌ **禁止使用工具的情况**: +- 用户说 "你好"、"嗨"、"hello" 等问候语 +- 用户进行闲聊或一般性提问 +- 用户没有提供具体路径时猜测路径 +- 为了 "了解环境" 或 "打招呼" 而读取文件 + +## 可用工具 + +- **read_file**:读取用户指定的文件或目录内容 +- **write_file**:创建或覆盖用户指定的文件 +- **edit_file**:修改用户指定文件的特定内容 +- **bash**:执行用户要求的 shell 命令 + + + +## 正确示例 + +用户: "你好" +助手: "你好!有什么我可以帮助你的吗?" +(直接文字回复,不调用任何工具) + +用户: "看看 /tmp/test.txt" +助手: 调用 read_file 工具读取 /tmp/test.txt + +用户: "帮我列出当前目录" +助手: "请告诉我你想查看哪个目录?" +(询问具体路径,不要猜测) + + + +- 使用 Markdown 格式 +- 回复简洁明了 +- 使用中文 +"#; + /// 聊天请求 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NativeChatRequest { diff --git a/src-tauri/src/app/commands/api_test.rs b/src-tauri/src/app/commands/api_test.rs index 1f5601151..4e009c805 100644 --- a/src-tauri/src/app/commands/api_test.rs +++ b/src-tauri/src/app/commands/api_test.rs @@ -3,6 +3,7 @@ //! 包含 API 测试、模型列表和兼容性检查命令。 use crate::app::types::{AppState, LogState, ProviderType}; +use crate::commands::model_registry_cmd::ModelRegistryState; /// 测试结果 #[derive(serde::Serialize)] @@ -279,82 +280,24 @@ pub async fn check_api_compatibility( /// 获取可用模型列表 #[tauri::command] -pub async fn get_available_models() -> Result, String> { - Ok(vec![ - // Kiro/Claude models - ModelInfo { - id: "claude-sonnet-4-5".to_string(), +pub async fn get_available_models( + state: tauri::State<'_, ModelRegistryState>, +) -> Result, String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + let models = service.get_all_models().await; + + Ok(models + .into_iter() + .map(|m| ModelInfo { + id: m.id, object: "model".to_string(), - owned_by: "anthropic".to_string(), - }, - ModelInfo { - id: "claude-sonnet-4-5-20250514".to_string(), - object: "model".to_string(), - owned_by: "anthropic".to_string(), - }, - ModelInfo { - id: "claude-sonnet-4-5-20250929".to_string(), - object: "model".to_string(), - owned_by: "anthropic".to_string(), - }, - ModelInfo { - id: "claude-3-7-sonnet-20250219".to_string(), - object: "model".to_string(), - owned_by: "anthropic".to_string(), - }, - ModelInfo { - id: "claude-3-5-sonnet-latest".to_string(), - object: "model".to_string(), - owned_by: "anthropic".to_string(), - }, - ModelInfo { - id: "claude-opus-4-5-20250514".to_string(), - object: "model".to_string(), - owned_by: "anthropic".to_string(), - }, - ModelInfo { - id: "claude-haiku-4-5-20250514".to_string(), - object: "model".to_string(), - owned_by: "anthropic".to_string(), - }, - // Gemini models - ModelInfo { - id: "gemini-2.5-flash".to_string(), - object: "model".to_string(), - owned_by: "google".to_string(), - }, - ModelInfo { - id: "gemini-2.5-flash-lite".to_string(), - object: "model".to_string(), - owned_by: "google".to_string(), - }, - ModelInfo { - id: "gemini-2.5-pro".to_string(), - object: "model".to_string(), - owned_by: "google".to_string(), - }, - ModelInfo { - id: "gemini-2.5-pro-preview-06-05".to_string(), - object: "model".to_string(), - owned_by: "google".to_string(), - }, - ModelInfo { - id: "gemini-3-pro-preview".to_string(), - object: "model".to_string(), - owned_by: "google".to_string(), - }, - // Qwen models - ModelInfo { - id: "qwen3-coder-plus".to_string(), - object: "model".to_string(), - owned_by: "alibaba".to_string(), - }, - ModelInfo { - id: "qwen3-coder-flash".to_string(), - object: "model".to_string(), - owned_by: "alibaba".to_string(), - }, - ]) + owned_by: m.provider_id, + }) + .collect()) } /// 测试 API diff --git a/src-tauri/src/app/commands/config.rs b/src-tauri/src/app/commands/config.rs index 25df3d6b8..5681cb09d 100644 --- a/src-tauri/src/app/commands/config.rs +++ b/src-tauri/src/app/commands/config.rs @@ -2,7 +2,7 @@ //! //! 包含配置读取、保存、Provider 设置等命令。 -use crate::app::types::{AppState, LogState, ProviderType}; +use crate::app::types::{AppState, LogState}; use crate::config; /// 获取配置 @@ -51,8 +51,8 @@ pub async fn set_default_provider( logs: tauri::State<'_, LogState>, provider: String, ) -> Result { - // 使用枚举验证 provider - let provider_type: ProviderType = provider.parse().map_err(|e: String| e)?; + // 允许任意 Provider ID(包括自定义 Provider 的 UUID) + // 不再强制验证为已知的 ProviderType let mut s = state.write().await; s.config.default_provider = provider.clone(); @@ -66,7 +66,7 @@ pub async fn set_default_provider( config::save_config(&s.config).map_err(|e| e.to_string())?; logs.write() .await - .add("info", &format!("默认 Provider 已切换为: {provider_type}")); + .add("info", &format!("默认 Provider 已切换为: {provider}")); Ok(provider) } @@ -95,12 +95,8 @@ pub async fn set_endpoint_provider( endpoint: String, provider: Option, ) -> Result { - // 验证 provider(如果提供) - if let Some(ref p) = provider { - if !p.is_empty() { - let _: ProviderType = p.parse().map_err(|e: String| e)?; - } - } + // 允许任意 Provider ID(包括自定义 Provider 的 UUID) + // 不再强制验证为已知的 ProviderType let mut s = state.write().await; diff --git a/src-tauri/src/app/runner.rs b/src-tauri/src/app/runner.rs index 74474dd9e..f03e25cd1 100644 --- a/src-tauri/src/app/runner.rs +++ b/src-tauri/src/app/runner.rs @@ -687,9 +687,6 @@ pub fn run() { commands::route_cmd::get_available_routes, commands::route_cmd::get_route_curl_examples, // Router config commands - commands::router_cmd::get_model_aliases, - commands::router_cmd::add_model_alias, - commands::router_cmd::remove_model_alias, commands::router_cmd::get_routing_rules, commands::router_cmd::add_routing_rule, commands::router_cmd::remove_routing_rule, @@ -698,8 +695,6 @@ pub fn run() { commands::router_cmd::add_exclusion, commands::router_cmd::remove_exclusion, commands::router_cmd::set_router_default_provider, - commands::router_cmd::get_recommended_presets, - commands::router_cmd::apply_recommended_preset, commands::router_cmd::clear_all_routing_config, // Resilience config commands commands::resilience_cmd::get_retry_config, diff --git a/src-tauri/src/commands/agent_cmd.rs b/src-tauri/src/commands/agent_cmd.rs index fb3f96c0a..6de78bcc6 100644 --- a/src-tauri/src/commands/agent_cmd.rs +++ b/src-tauri/src/commands/agent_cmd.rs @@ -52,7 +52,12 @@ pub async fn agent_start_process( let base_url = format!("http://127.0.0.1:{}", port); let provider_type = ProviderType::from_str(&default_provider); - agent_state.init(base_url.clone(), api_key, provider_type)?; + agent_state.init( + base_url.clone(), + api_key, + provider_type, + Some(default_provider), + )?; Ok(AgentProcessStatus { running: true, @@ -137,7 +142,7 @@ pub async fn agent_create_session( let api_key = api_key.ok_or_else(|| "未配置 API Key".to_string())?; let base_url = format!("http://127.0.0.1:{}", port); let provider_type = ProviderType::from_str(&default_provider); - agent_state.init(base_url, api_key, provider_type)?; + agent_state.init(base_url, api_key, provider_type, Some(default_provider))?; } // 构建包含 Skills 的 System Prompt @@ -243,7 +248,7 @@ pub async fn agent_send_message( let api_key = api_key.ok_or_else(|| "未配置 API Key".to_string())?; let base_url = format!("http://127.0.0.1:{}", port); let provider_type = ProviderType::from_str(&default_provider); - agent_state.init(base_url, api_key, provider_type)?; + agent_state.init(base_url, api_key, provider_type, Some(default_provider))?; } // 根据启用的模式构建最终消息 diff --git a/src-tauri/src/commands/model_registry_cmd.rs b/src-tauri/src/commands/model_registry_cmd.rs index 24e1e4b24..e219df894 100644 --- a/src-tauri/src/commands/model_registry_cmd.rs +++ b/src-tauri/src/commands/model_registry_cmd.rs @@ -34,7 +34,7 @@ pub async fn refresh_model_registry(state: State<'_, ModelRegistryState>) -> Res .as_ref() .ok_or_else(|| "模型注册服务未初始化".to_string())?; - service.refresh_from_models_dev().await + service.refresh_from_repo().await } /// 搜索模型 diff --git a/src-tauri/src/commands/native_agent_cmd.rs b/src-tauri/src/commands/native_agent_cmd.rs index 78a406a32..fbddea688 100644 --- a/src-tauri/src/commands/native_agent_cmd.rs +++ b/src-tauri/src/commands/native_agent_cmd.rs @@ -6,6 +6,8 @@ use crate::agent::{ AgentSession, ImageData, NativeAgentState, NativeChatRequest, NativeChatResponse, ProviderType, StreamEvent, ToolLoopEngine, }; +use crate::database::dao::api_key_provider::ApiKeyProviderDao; +use crate::database::DbConnection; use crate::AppState; use serde::{Deserialize, Serialize}; use tauri::{Emitter, State}; @@ -24,13 +26,14 @@ pub async fn native_agent_init( ) -> Result { tracing::info!("[NativeAgent] 初始化 Agent"); - let (port, api_key, running, default_provider) = { + let (port, api_key, running, default_provider, agent_config) = { let state = app_state.read().await; ( state.config.server.port, state.running_api_key.clone(), state.running, state.config.routing.default_provider.clone(), + state.config.agent.clone(), ) }; @@ -44,12 +47,20 @@ pub async fn native_agent_init( let provider_type = ProviderType::from_str(&default_provider); tracing::info!( - "[NativeAgent] 初始化 Agent: base_url={}, provider={:?}", + "[NativeAgent] 初始化 Agent: base_url={}, provider={:?}, use_default_prompt={}", base_url, - provider_type + provider_type, + agent_config.use_default_system_prompt ); - agent_state.init(base_url.clone(), api_key, provider_type)?; + // 使用带配置的初始化方法 + agent_state.init_with_config( + base_url.clone(), + api_key, + provider_type, + Some(default_provider), + &agent_config, + )?; tracing::info!("[NativeAgent] Agent 初始化成功: {}", base_url); @@ -115,7 +126,7 @@ pub async fn native_agent_chat( let api_key = api_key.ok_or_else(|| "未配置 API Key".to_string())?; let base_url = format!("http://127.0.0.1:{}", port); let provider_type = ProviderType::from_str(&default_provider); - agent_state.init(base_url, api_key, provider_type)?; + agent_state.init(base_url, api_key, provider_type, Some(default_provider))?; } let request = NativeChatRequest { @@ -142,6 +153,7 @@ pub async fn native_agent_chat_stream( app_handle: tauri::AppHandle, agent_state: State<'_, NativeAgentState>, app_state: State<'_, AppState>, + db: State<'_, DbConnection>, message: String, event_name: String, session_id: Option, @@ -177,7 +189,25 @@ pub async fn native_agent_chat_stream( // 使用前端传递的 provider,如果没有则使用默认值 let provider_str = provider.unwrap_or(default_provider); - let provider_type = ProviderType::from_str(&provider_str); + + // 尝试从数据库查询 Provider 的类型(用于确定协议) + let provider_type = { + let conn = db.lock().map_err(|e| format!("数据库锁定失败: {}", e))?; + if let Ok(Some(api_provider)) = ApiKeyProviderDao::get_provider_by_id(&conn, &provider_str) + { + // 根据 API Key Provider 的 type 确定协议 + let api_type = api_provider.provider_type.to_string(); + tracing::info!( + "[NativeAgent] 从数据库获取 Provider 类型: {} -> {}", + provider_str, + api_type + ); + ProviderType::from_str(&api_type) + } else { + // 数据库中没有找到,使用默认解析 + ProviderType::from_str(&provider_str) + } + }; tracing::info!( "[NativeAgent] 使用 provider: {:?} (原始值: {})", @@ -186,15 +216,16 @@ pub async fn native_agent_chat_stream( ); // 如果 Agent 未初始化,或者 provider 发生变化,重新初始化 + // 使用 provider_str 而不是 provider_type 来判断,因为自定义 Provider 的 type 都是 OpenAI let need_reinit = if !agent_state.is_initialized() { tracing::info!("[NativeAgent] Agent 未初始化,需要初始化"); true - } else if let Some(current_provider) = agent_state.get_provider_type() { - if current_provider != provider_type { + } else if let Some(current_provider_id) = agent_state.get_provider_id() { + if current_provider_id != provider_str { tracing::info!( - "[NativeAgent] Provider 发生变化: {:?} -> {:?},需要重新初始化", - current_provider, - provider_type + "[NativeAgent] Provider 发生变化: {} -> {},需要重新初始化", + current_provider_id, + provider_str ); true } else { @@ -206,7 +237,7 @@ pub async fn native_agent_chat_stream( if need_reinit { let base_url = format!("http://127.0.0.1:{}", port); - agent_state.init(base_url, api_key, provider_type)?; + agent_state.init(base_url, api_key, provider_type, Some(provider_str.clone()))?; } // 获取工具注册表(用于创建 ToolLoopEngine) diff --git a/src-tauri/src/commands/oauth_plugin_cmd.rs b/src-tauri/src/commands/oauth_plugin_cmd.rs index 107648c73..5b6ed7ffc 100644 --- a/src-tauri/src/commands/oauth_plugin_cmd.rs +++ b/src-tauri/src/commands/oauth_plugin_cmd.rs @@ -925,9 +925,18 @@ pub async fn plugin_config_set( pub async fn read_plugin_ui_file(path: String) -> Result { use std::fs; - // 安全检查:确保路径在插件目录内 - let path = std::path::PathBuf::from(&path); + // 展开 ~ 为用户主目录 + let expanded_path = if path.starts_with("~/") { + if let Some(home) = dirs::home_dir() { + home.join(&path[2..]) + } else { + std::path::PathBuf::from(&path) + } + } else { + std::path::PathBuf::from(&path) + }; // 读取文件内容 - fs::read_to_string(&path).map_err(|e| format!("读取插件 UI 文件失败: {}", e)) + fs::read_to_string(&expanded_path) + .map_err(|e| format!("读取插件 UI 文件失败: {} (路径: {:?})", e, expanded_path)) } diff --git a/src-tauri/src/commands/router_cmd.rs b/src-tauri/src/commands/router_cmd.rs index ecfe0a3f2..b4b5f3287 100644 --- a/src-tauri/src/commands/router_cmd.rs +++ b/src-tauri/src/commands/router_cmd.rs @@ -6,13 +6,6 @@ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -/// 模型别名 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModelAlias { - pub alias: String, - pub actual: String, -} - /// 路由规则 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RoutingRuleDto { @@ -24,7 +17,6 @@ pub struct RoutingRuleDto { /// 路由配置状态 pub struct RouterConfigState { - pub aliases: Arc>>, pub rules: Arc>>, pub exclusions: Arc>>>, } @@ -32,51 +24,12 @@ pub struct RouterConfigState { impl Default for RouterConfigState { fn default() -> Self { Self { - aliases: Arc::new(RwLock::new(HashMap::new())), rules: Arc::new(RwLock::new(Vec::new())), exclusions: Arc::new(RwLock::new(HashMap::new())), } } } -/// 获取所有模型别名 -#[tauri::command] -pub async fn get_model_aliases( - state: tauri::State<'_, RouterConfigState>, -) -> Result, String> { - let aliases = state.aliases.read().await; - Ok(aliases - .iter() - .map(|(alias, actual)| ModelAlias { - alias: alias.clone(), - actual: actual.clone(), - }) - .collect()) -} - -/// 添加模型别名 -#[tauri::command] -pub async fn add_model_alias( - state: tauri::State<'_, RouterConfigState>, - alias: String, - actual: String, -) -> Result<(), String> { - let mut aliases = state.aliases.write().await; - aliases.insert(alias, actual); - Ok(()) -} - -/// 移除模型别名 -#[tauri::command] -pub async fn remove_model_alias( - state: tauri::State<'_, RouterConfigState>, - alias: String, -) -> Result<(), String> { - let mut aliases = state.aliases.write().await; - aliases.remove(&alias); - Ok(()) -} - /// 获取所有路由规则 #[tauri::command] pub async fn get_routing_rules( @@ -178,404 +131,11 @@ pub async fn set_router_default_provider(_provider: ProviderType) -> Result<(), Ok(()) } -/// 推荐配置预设 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecommendedPreset { - pub id: String, - pub name: String, - pub description: String, - pub aliases: Vec, - pub rules: Vec, - /// 客户端路由配置 - #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint_providers: Option, -} - -/// 端点 Provider 配置 DTO -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct EndpointProvidersConfigDto { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cursor: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub claude_code: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub codex: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub windsurf: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub kiro: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub other: Option, -} - -/// 获取推荐配置列表 -#[tauri::command] -pub async fn get_recommended_presets() -> Result, String> { - Ok(vec![ - RecommendedPreset { - id: "claude-optimized".to_string(), - name: "Claude 优化配置".to_string(), - description: "将所有 Claude 模型请求路由到 Kiro,适合主要使用 Claude 的用户" - .to_string(), - aliases: vec![ - // Claude 4.5 系列 (最新) - ModelAlias { - alias: "claude".to_string(), - actual: "claude-opus-4-5".to_string(), - }, - ModelAlias { - alias: "opus".to_string(), - actual: "claude-opus-4-5".to_string(), - }, - ModelAlias { - alias: "sonnet".to_string(), - actual: "claude-sonnet-4-5".to_string(), - }, - ModelAlias { - alias: "haiku".to_string(), - actual: "claude-haiku-4-5".to_string(), - }, - // Claude 4 系列 - ModelAlias { - alias: "opus-4".to_string(), - actual: "claude-opus-4".to_string(), - }, - ModelAlias { - alias: "sonnet-4".to_string(), - actual: "claude-sonnet-4".to_string(), - }, - // Claude 3.7/3.5 系列 (旧版) - ModelAlias { - alias: "sonnet-3.7".to_string(), - actual: "claude-3-7-sonnet-latest".to_string(), - }, - ModelAlias { - alias: "sonnet-3.5".to_string(), - actual: "claude-3-5-sonnet-latest".to_string(), - }, - ], - rules: vec![ - RoutingRuleDto { - pattern: "claude-*".to_string(), - target_provider: ProviderType::Kiro, - priority: 1, - enabled: true, - }, - RoutingRuleDto { - pattern: "*sonnet*".to_string(), - target_provider: ProviderType::Kiro, - priority: 2, - enabled: true, - }, - RoutingRuleDto { - pattern: "*opus*".to_string(), - target_provider: ProviderType::Kiro, - priority: 2, - enabled: true, - }, - RoutingRuleDto { - pattern: "*haiku*".to_string(), - target_provider: ProviderType::Kiro, - priority: 2, - enabled: true, - }, - ], - endpoint_providers: None, - }, - RecommendedPreset { - id: "gemini-optimized".to_string(), - name: "Gemini 优化配置".to_string(), - description: "将 Gemini 模型请求路由到 Gemini Provider,适合主要使用 Google AI 的用户" - .to_string(), - aliases: vec![ - // Gemini 3 系列 (最新) - ModelAlias { - alias: "gemini".to_string(), - actual: "gemini-3-pro".to_string(), - }, - ModelAlias { - alias: "gemini-pro".to_string(), - actual: "gemini-3-pro".to_string(), - }, - ModelAlias { - alias: "gemini-3".to_string(), - actual: "gemini-3-pro".to_string(), - }, - // Gemini 2.5 系列 - ModelAlias { - alias: "flash".to_string(), - actual: "gemini-2.5-flash".to_string(), - }, - ModelAlias { - alias: "flash-lite".to_string(), - actual: "gemini-2.5-flash-lite".to_string(), - }, - ModelAlias { - alias: "gemini-2.5".to_string(), - actual: "gemini-2.5-pro".to_string(), - }, - ], - rules: vec![ - RoutingRuleDto { - pattern: "gemini-*".to_string(), - target_provider: ProviderType::Gemini, - priority: 1, - enabled: true, - }, - RoutingRuleDto { - pattern: "*flash*".to_string(), - target_provider: ProviderType::Gemini, - priority: 2, - enabled: true, - }, - ], - endpoint_providers: None, - }, - RecommendedPreset { - id: "multi-provider".to_string(), - name: "多 Provider 均衡配置".to_string(), - description: "根据模型名称自动路由到对应的 Provider,适合同时使用多个 AI 服务的用户" - .to_string(), - aliases: vec![ - // Claude (最新) - ModelAlias { - alias: "claude".to_string(), - actual: "claude-opus-4-5".to_string(), - }, - ModelAlias { - alias: "sonnet".to_string(), - actual: "claude-sonnet-4-5".to_string(), - }, - // Gemini (最新) - ModelAlias { - alias: "gemini".to_string(), - actual: "gemini-3-pro".to_string(), - }, - ModelAlias { - alias: "flash".to_string(), - actual: "gemini-2.5-flash".to_string(), - }, - // Qwen - ModelAlias { - alias: "qwen".to_string(), - actual: "qwen3-coder-plus".to_string(), - }, - // OpenAI (最新) - ModelAlias { - alias: "gpt".to_string(), - actual: "gpt-5.2".to_string(), - }, - ModelAlias { - alias: "gpt-5".to_string(), - actual: "gpt-5.2".to_string(), - }, - ModelAlias { - alias: "gpt-4".to_string(), - actual: "gpt-4o".to_string(), - }, - ModelAlias { - alias: "o1".to_string(), - actual: "o1".to_string(), - }, - ModelAlias { - alias: "o3".to_string(), - actual: "o3".to_string(), - }, - ], - rules: vec![ - RoutingRuleDto { - pattern: "claude-*".to_string(), - target_provider: ProviderType::Kiro, - priority: 1, - enabled: true, - }, - RoutingRuleDto { - pattern: "gemini-*".to_string(), - target_provider: ProviderType::Gemini, - priority: 1, - enabled: true, - }, - RoutingRuleDto { - pattern: "qwen*".to_string(), - target_provider: ProviderType::Qwen, - priority: 1, - enabled: true, - }, - RoutingRuleDto { - pattern: "gpt-*".to_string(), - target_provider: ProviderType::OpenAI, - priority: 1, - enabled: true, - }, - RoutingRuleDto { - pattern: "o1*".to_string(), - target_provider: ProviderType::OpenAI, - priority: 1, - enabled: true, - }, - RoutingRuleDto { - pattern: "o3*".to_string(), - target_provider: ProviderType::OpenAI, - priority: 1, - enabled: true, - }, - ], - endpoint_providers: None, - }, - RecommendedPreset { - id: "coding-assistant".to_string(), - name: "编程助手配置".to_string(), - description: - "针对编程场景优化,Claude Opus 4.5 用于复杂代码,Gemini Flash 用于快速响应" - .to_string(), - aliases: vec![ - ModelAlias { - alias: "code".to_string(), - actual: "claude-opus-4-5".to_string(), - }, - ModelAlias { - alias: "coder".to_string(), - actual: "qwen3-coder-plus".to_string(), - }, - ModelAlias { - alias: "fast".to_string(), - actual: "gemini-2.5-flash".to_string(), - }, - ModelAlias { - alias: "think".to_string(), - actual: "claude-sonnet-4-5".to_string(), - }, - ], - rules: vec![ - RoutingRuleDto { - pattern: "*coder*".to_string(), - target_provider: ProviderType::Qwen, - priority: 1, - enabled: true, - }, - RoutingRuleDto { - pattern: "claude-*".to_string(), - target_provider: ProviderType::Kiro, - priority: 2, - enabled: true, - }, - RoutingRuleDto { - pattern: "gemini-*".to_string(), - target_provider: ProviderType::Gemini, - priority: 2, - enabled: true, - }, - ], - endpoint_providers: None, - }, - RecommendedPreset { - id: "cost-effective".to_string(), - name: "性价比优先配置".to_string(), - description: "优先使用免费或低成本的模型,适合预算有限的用户".to_string(), - aliases: vec![ - ModelAlias { - alias: "default".to_string(), - actual: "gemini-2.5-flash".to_string(), - }, - ModelAlias { - alias: "cheap".to_string(), - actual: "gemini-2.5-flash-lite".to_string(), - }, - ModelAlias { - alias: "free".to_string(), - actual: "gemini-2.5-flash".to_string(), - }, - ], - rules: vec![ - // 默认路由到 Gemini(免费额度高) - RoutingRuleDto { - pattern: "*".to_string(), - target_provider: ProviderType::Gemini, - priority: 100, - enabled: true, - }, - // Claude 请求仍然路由到 Kiro - RoutingRuleDto { - pattern: "claude-*".to_string(), - target_provider: ProviderType::Kiro, - priority: 1, - enabled: true, - }, - ], - endpoint_providers: None, - }, - // 客户端路由预设 - RecommendedPreset { - id: "client-routing".to_string(), - name: "客户端路由配置".to_string(), - description: "为不同的 IDE 客户端配置不同的 Provider,Cursor/Windsurf 使用 Kiro,Claude Code 使用 Kiro,Codex 使用 OpenAI" - .to_string(), - aliases: vec![], - rules: vec![], - endpoint_providers: Some(EndpointProvidersConfigDto { - cursor: Some("kiro".to_string()), - claude_code: Some("kiro".to_string()), - codex: Some("openai".to_string()), - windsurf: Some("kiro".to_string()), - kiro: Some("kiro".to_string()), - other: None, - }), - }, - ]) -} - -/// 应用推荐配置 -#[tauri::command] -pub async fn apply_recommended_preset( - state: tauri::State<'_, RouterConfigState>, - preset_id: String, - merge: bool, -) -> Result<(), String> { - let presets = get_recommended_presets().await?; - let preset = presets - .into_iter() - .find(|p| p.id == preset_id) - .ok_or_else(|| format!("未找到预设配置: {}", preset_id))?; - - // 应用别名 - { - let mut aliases = state.aliases.write().await; - if !merge { - aliases.clear(); - } - for alias in preset.aliases { - aliases.insert(alias.alias, alias.actual); - } - } - - // 应用规则 - { - let mut rules = state.rules.write().await; - if !merge { - rules.clear(); - } - for rule in preset.rules { - // 避免重复 - if !rules.iter().any(|r| r.pattern == rule.pattern) { - rules.push(rule); - } - } - // 按优先级排序 - rules.sort_by(|a, b| a.priority.cmp(&b.priority)); - } - - Ok(()) -} - /// 清空所有路由配置 #[tauri::command] pub async fn clear_all_routing_config( state: tauri::State<'_, RouterConfigState>, ) -> Result<(), String> { - { - let mut aliases = state.aliases.write().await; - aliases.clear(); - } { let mut rules = state.rules.write().await; rules.clear(); diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs index 78138d951..285be6149 100644 --- a/src-tauri/src/config/mod.rs +++ b/src-tauri/src/config/mod.rs @@ -22,9 +22,9 @@ pub use types::{ generate_secure_api_key, AmpConfig, AmpModelMapping, ApiKeyEntry, Config, CredentialEntry, CredentialPoolConfig, CustomProviderConfig, EndpointProvidersConfig, GeminiApiKeyEntry, IFlowCredentialEntry, InjectionRuleConfig, InjectionSettings, LoggingConfig, ModelInfo, - ModelsConfig, ProviderConfig, ProviderModelsConfig, ProvidersConfig, QuotaExceededConfig, - RemoteManagementConfig, RetrySettings, RoutingConfig, RoutingRuleConfig, ServerConfig, - TlsConfig, VertexApiKeyEntry, VertexModelAlias, DEFAULT_API_KEY, + ModelsConfig, NativeAgentConfig, ProviderConfig, ProviderModelsConfig, ProvidersConfig, + QuotaExceededConfig, RemoteManagementConfig, RetrySettings, RoutingConfig, RoutingRuleConfig, + ServerConfig, TlsConfig, VertexApiKeyEntry, VertexModelAlias, DEFAULT_API_KEY, }; pub use yaml::{load_config, save_config, ConfigError, ConfigManager, YamlService}; diff --git a/src-tauri/src/config/tests.rs b/src-tauri/src/config/tests.rs index 22e93d842..e67a06985 100644 --- a/src-tauri/src/config/tests.rs +++ b/src-tauri/src/config/tests.rs @@ -220,6 +220,7 @@ fn arb_config() -> impl Strategy { endpoint_providers: crate::config::EndpointProvidersConfig::default(), minimize_to_tray: true, models: crate::config::ModelsConfig::default(), + agent: crate::config::NativeAgentConfig::default(), }) } @@ -494,6 +495,7 @@ fn arb_valid_config() -> impl Strategy { endpoint_providers: crate::config::EndpointProvidersConfig::default(), minimize_to_tray: true, models: crate::config::ModelsConfig::default(), + agent: crate::config::NativeAgentConfig::default(), }) } @@ -540,6 +542,7 @@ fn arb_invalid_config() -> impl Strategy { endpoint_providers: crate::config::EndpointProvidersConfig::default(), minimize_to_tray: true, models: crate::config::ModelsConfig::default(), + agent: crate::config::NativeAgentConfig::default(), }; // 根据类型使配置无效 match invalid_type { diff --git a/src-tauri/src/config/types.rs b/src-tauri/src/config/types.rs index 105751366..2e24c752e 100644 --- a/src-tauri/src/config/types.rs +++ b/src-tauri/src/config/types.rs @@ -314,6 +314,102 @@ pub struct Config { /// 模型配置(动态加载 Provider 和模型列表) #[serde(default)] pub models: ModelsConfig, + /// Native Agent 配置 + #[serde(default)] + pub agent: NativeAgentConfig, +} + +// ============ Native Agent 配置类型 ============ + +/// Native Agent 配置 +/// +/// 配置内置 Agent 的行为,包括系统提示词、工具使用规则等 +/// 参考 Manus Agent 的模块化设计,支持灵活配置 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct NativeAgentConfig { + /// 是否使用默认系统提示词 + /// 当 custom_system_prompt 为空时,如果此项为 true 则使用内置默认提示词 + #[serde(default = "default_use_default_prompt")] + pub use_default_system_prompt: bool, + /// 自定义系统提示词 + /// 如果设置了此项,将覆盖默认系统提示词 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub custom_system_prompt: Option, + /// 系统提示词模板文件路径(支持 ~ 展开) + /// 可以将系统提示词存储在外部文件中,便于管理和版本控制 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub system_prompt_file: Option, + /// 默认模型 + #[serde(default = "default_agent_model")] + pub default_model: String, + /// 默认温度参数 + #[serde(default = "default_temperature")] + pub temperature: f32, + /// 默认最大 token 数 + #[serde(default = "default_max_tokens")] + pub max_tokens: u32, +} + +fn default_use_default_prompt() -> bool { + true +} + +fn default_agent_model() -> String { + "claude-sonnet-4-20250514".to_string() +} + +fn default_temperature() -> f32 { + 0.7 +} + +fn default_max_tokens() -> u32 { + 4096 +} + +impl Default for NativeAgentConfig { + fn default() -> Self { + Self { + use_default_system_prompt: default_use_default_prompt(), + custom_system_prompt: None, + system_prompt_file: None, + default_model: default_agent_model(), + temperature: default_temperature(), + max_tokens: default_max_tokens(), + } + } +} + +impl NativeAgentConfig { + /// 获取有效的系统提示词 + /// + /// 优先级: + /// 1. system_prompt_file(外部文件) + /// 2. custom_system_prompt(配置中的自定义提示词) + /// 3. 如果 use_default_system_prompt 为 true,返回 None 让调用方使用默认提示词 + /// 4. 否则返回 None(不使用任何系统提示词) + pub fn get_effective_system_prompt(&self) -> Option { + // 优先从文件加载 + if let Some(file_path) = &self.system_prompt_file { + let expanded_path = crate::config::expand_tilde(file_path); + if let Ok(content) = std::fs::read_to_string(&expanded_path) { + let trimmed = content.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + } + + // 其次使用配置中的自定义提示词 + if let Some(prompt) = &self.custom_system_prompt { + let trimmed = prompt.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + + // 返回 None,让调用方根据 use_default_system_prompt 决定是否使用默认提示词 + None + } } fn default_minimize_to_tray() -> bool { @@ -1116,6 +1212,7 @@ impl Default for Config { endpoint_providers: EndpointProvidersConfig::default(), minimize_to_tray: default_minimize_to_tray(), models: ModelsConfig::default(), + agent: NativeAgentConfig::default(), } } } diff --git a/src-tauri/src/credential/registry.rs b/src-tauri/src/credential/registry.rs index 26fd9a2bc..dae980d08 100644 --- a/src-tauri/src/credential/registry.rs +++ b/src-tauri/src/credential/registry.rs @@ -754,13 +754,56 @@ impl CredentialProviderRegistry { /// 检查插件更新 pub async fn check_updates(&self) -> OAuthPluginResult> { - // TODO: 实现更新检查逻辑 - // 1. 遍历所有插件 - // 2. 检查 GitHub Release 或其他来源 - // 3. 比较版本号 - // 4. 返回有更新的插件列表 + // 已知插件的最新版本(与前端 OAuthPluginTab.tsx 保持同步) + let latest_versions: std::collections::HashMap<&str, &str> = [ + ("kiro-provider", "0.3.0"), + ("antigravity-provider", "0.4.0"), + ("claude-provider", "0.3.0"), + ("droid-provider", "0.3.0"), + ("gemini-provider", "0.4.0"), + ("codex-provider", "0.1.0"), + ] + .into_iter() + .collect(); - Ok(vec![]) + let mut updates = Vec::new(); + + // 扫描已安装的插件 + if let Ok(entries) = std::fs::read_dir(&self.plugins_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let plugin_json = path.join("plugin.json"); + if plugin_json.exists() { + if let Ok(content) = std::fs::read_to_string(&plugin_json) { + if let Ok(manifest) = + serde_json::from_str::(&content) + { + let plugin_id = manifest["name"].as_str().unwrap_or_default(); + let current_version = + manifest["version"].as_str().unwrap_or("0.0.0"); + + // 检查是否有更新 + if let Some(&latest) = latest_versions.get(plugin_id) { + if version_compare(current_version, latest) + == std::cmp::Ordering::Less + { + updates.push(PluginUpdate { + plugin_id: plugin_id.to_string(), + current_version: current_version.to_string(), + latest_version: latest.to_string(), + changelog: None, + }); + } + } + } + } + } + } + } + } + + Ok(updates) } // ======================================================================== @@ -809,6 +852,29 @@ pub fn get_global_registry() -> Option> { GLOBAL_REGISTRY.get().cloned() } +/// 比较语义化版本号 +fn version_compare(v1: &str, v2: &str) -> std::cmp::Ordering { + let parse = |v: &str| -> Vec { + v.trim_start_matches('v') + .split('.') + .filter_map(|s| s.parse().ok()) + .collect() + }; + + let v1_parts = parse(v1); + let v2_parts = parse(v2); + + for i in 0..std::cmp::max(v1_parts.len(), v2_parts.len()) { + let p1 = v1_parts.get(i).copied().unwrap_or(0); + let p2 = v2_parts.get(i).copied().unwrap_or(0); + match p1.cmp(&p2) { + std::cmp::Ordering::Equal => continue, + other => return other, + } + } + std::cmp::Ordering::Equal +} + /// 递归复制目录 fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> { std::fs::create_dir_all(dst)?; diff --git a/src-tauri/src/data/local_models.rs b/src-tauri/src/data/local_models.rs deleted file mode 100644 index 0c6eb3b7c..000000000 --- a/src-tauri/src/data/local_models.rs +++ /dev/null @@ -1,984 +0,0 @@ -//! 本地硬编码的国内模型数据 -//! -//! 这些模型数据用于补充 models.dev API 未覆盖的国内模型 - -use crate::models::model_registry::{ - EnhancedModelMetadata, ModelCapabilities, ModelLimits, ModelPricing, ModelSource, ModelStatus, - ModelTier, -}; - -/// 获取所有本地硬编码的国内模型 -pub fn get_local_models() -> Vec { - let mut models = Vec::new(); - models.extend(get_dashscope_models()); - models.extend(get_zhipu_models()); - models.extend(get_baichuan_models()); - models.extend(get_moonshot_models()); - models.extend(get_deepseek_models()); - models.extend(get_doubao_models()); - models.extend(get_minimax_models()); - models.extend(get_yi_models()); - models.extend(get_stepfun_models()); - models -} - -/// 通义千问系列模型 (阿里云百炼) -fn get_dashscope_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![ - EnhancedModelMetadata { - id: "qwen3-coder-plus".to_string(), - display_name: "通义千问 Coder Plus".to_string(), - provider_id: "dashscope".to_string(), - provider_name: "阿里云百炼".to_string(), - family: Some("qwen-coder".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(4.0), - output_per_million: Some(16.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(131072), - max_output_tokens: Some(8192), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2025-01-01".to_string()), - is_latest: true, - description: Some("阿里云通义千问代码模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "qwen-max".to_string(), - display_name: "通义千问 Max".to_string(), - provider_id: "dashscope".to_string(), - provider_name: "阿里云百炼".to_string(), - family: Some("qwen".to_string()), - tier: ModelTier::Max, - capabilities: ModelCapabilities { - vision: true, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: true, - }, - pricing: Some(ModelPricing { - input_per_million: Some(20.0), - output_per_million: Some(60.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(32768), - max_output_tokens: Some(8192), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-12-01".to_string()), - is_latest: true, - description: Some("通义千问旗舰模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "qwen-plus".to_string(), - display_name: "通义千问 Plus".to_string(), - provider_id: "dashscope".to_string(), - provider_name: "阿里云百炼".to_string(), - family: Some("qwen".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: true, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(4.0), - output_per_million: Some(12.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(131072), - max_output_tokens: Some(8192), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-12-01".to_string()), - is_latest: true, - description: Some("通义千问增强版".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "qwen-turbo".to_string(), - display_name: "通义千问 Turbo".to_string(), - provider_id: "dashscope".to_string(), - provider_name: "阿里云百炼".to_string(), - family: Some("qwen".to_string()), - tier: ModelTier::Mini, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(0.3), - output_per_million: Some(0.6), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(131072), - max_output_tokens: Some(8192), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-12-01".to_string()), - is_latest: true, - description: Some("通义千问快速版".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - ] -} - -/// 智谱 GLM 系列模型 -fn get_zhipu_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![ - EnhancedModelMetadata { - id: "glm-4-plus".to_string(), - display_name: "GLM-4 Plus".to_string(), - provider_id: "zhipu".to_string(), - provider_name: "智谱 AI".to_string(), - family: Some("glm-4".to_string()), - tier: ModelTier::Max, - capabilities: ModelCapabilities { - vision: true, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: true, - }, - pricing: Some(ModelPricing { - input_per_million: Some(50.0), - output_per_million: Some(50.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(128000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-08-01".to_string()), - is_latest: true, - description: Some("智谱旗舰模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "glm-4-air".to_string(), - display_name: "GLM-4 Air".to_string(), - provider_id: "zhipu".to_string(), - provider_name: "智谱 AI".to_string(), - family: Some("glm-4".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(1.0), - output_per_million: Some(1.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(128000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("智谱高性价比模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "glm-4-flash".to_string(), - display_name: "GLM-4 Flash".to_string(), - provider_id: "zhipu".to_string(), - provider_name: "智谱 AI".to_string(), - family: Some("glm-4".to_string()), - tier: ModelTier::Mini, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(0.1), - output_per_million: Some(0.1), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(128000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("智谱快速模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - ] -} - -/// 百川系列模型 -fn get_baichuan_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![ - EnhancedModelMetadata { - id: "Baichuan4".to_string(), - display_name: "百川 4".to_string(), - provider_id: "baichuan".to_string(), - provider_name: "百川智能".to_string(), - family: Some("baichuan".to_string()), - tier: ModelTier::Max, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(100.0), - output_per_million: Some(100.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(32768), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-10-01".to_string()), - is_latest: true, - description: Some("百川旗舰模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "Baichuan3-Turbo".to_string(), - display_name: "百川 3 Turbo".to_string(), - provider_id: "baichuan".to_string(), - provider_name: "百川智能".to_string(), - family: Some("baichuan".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(12.0), - output_per_million: Some(12.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(32768), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("百川高性价比模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - ] -} - -/// 月之暗面 Moonshot 系列模型 -fn get_moonshot_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![ - EnhancedModelMetadata { - id: "moonshot-v1-128k".to_string(), - display_name: "Moonshot V1 128K".to_string(), - provider_id: "moonshot".to_string(), - provider_name: "月之暗面".to_string(), - family: Some("moonshot".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(60.0), - output_per_million: Some(60.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(128000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-03-01".to_string()), - is_latest: true, - description: Some("月之暗面长上下文模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "moonshot-v1-32k".to_string(), - display_name: "Moonshot V1 32K".to_string(), - provider_id: "moonshot".to_string(), - provider_name: "月之暗面".to_string(), - family: Some("moonshot".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(24.0), - output_per_million: Some(24.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(32000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-03-01".to_string()), - is_latest: true, - description: Some("月之暗面标准模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "moonshot-v1-8k".to_string(), - display_name: "Moonshot V1 8K".to_string(), - provider_id: "moonshot".to_string(), - provider_name: "月之暗面".to_string(), - family: Some("moonshot".to_string()), - tier: ModelTier::Mini, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(12.0), - output_per_million: Some(12.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(8000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-03-01".to_string()), - is_latest: true, - description: Some("月之暗面快速模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - ] -} - -/// DeepSeek 系列模型 -fn get_deepseek_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![ - EnhancedModelMetadata { - id: "deepseek-chat".to_string(), - display_name: "DeepSeek Chat".to_string(), - provider_id: "deepseek".to_string(), - provider_name: "DeepSeek".to_string(), - family: Some("deepseek".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(1.0), - output_per_million: Some(2.0), - cache_read_per_million: Some(0.1), - cache_write_per_million: Some(1.0), - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(64000), - max_output_tokens: Some(8192), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-12-01".to_string()), - is_latest: true, - description: Some("DeepSeek V3 对话模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "deepseek-reasoner".to_string(), - display_name: "DeepSeek Reasoner".to_string(), - provider_id: "deepseek".to_string(), - provider_name: "DeepSeek".to_string(), - family: Some("deepseek".to_string()), - tier: ModelTier::Max, - capabilities: ModelCapabilities { - vision: false, - tools: false, - streaming: true, - json_mode: false, - function_calling: false, - reasoning: true, - }, - pricing: Some(ModelPricing { - input_per_million: Some(4.0), - output_per_million: Some(16.0), - cache_read_per_million: Some(0.4), - cache_write_per_million: Some(4.0), - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(64000), - max_output_tokens: Some(8192), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2025-01-01".to_string()), - is_latest: true, - description: Some("DeepSeek R1 推理模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "deepseek-coder".to_string(), - display_name: "DeepSeek Coder".to_string(), - provider_id: "deepseek".to_string(), - provider_name: "DeepSeek".to_string(), - family: Some("deepseek-coder".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(1.0), - output_per_million: Some(2.0), - cache_read_per_million: Some(0.1), - cache_write_per_million: Some(1.0), - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(64000), - max_output_tokens: Some(8192), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("DeepSeek 代码模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - ] -} - -/// 字节豆包系列模型 -fn get_doubao_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![ - EnhancedModelMetadata { - id: "doubao-pro-256k".to_string(), - display_name: "豆包 Pro 256K".to_string(), - provider_id: "doubao".to_string(), - provider_name: "字节跳动".to_string(), - family: Some("doubao".to_string()), - tier: ModelTier::Max, - capabilities: ModelCapabilities { - vision: true, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(5.0), - output_per_million: Some(9.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(256000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-10-01".to_string()), - is_latest: true, - description: Some("豆包旗舰长上下文模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "doubao-pro-32k".to_string(), - display_name: "豆包 Pro 32K".to_string(), - provider_id: "doubao".to_string(), - provider_name: "字节跳动".to_string(), - family: Some("doubao".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: true, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(0.8), - output_per_million: Some(2.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(32000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("豆包标准模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "doubao-lite-32k".to_string(), - display_name: "豆包 Lite 32K".to_string(), - provider_id: "doubao".to_string(), - provider_name: "字节跳动".to_string(), - family: Some("doubao".to_string()), - tier: ModelTier::Mini, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(0.3), - output_per_million: Some(0.6), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(32000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("豆包轻量模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - ] -} - -/// MiniMax 系列模型 -fn get_minimax_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![EnhancedModelMetadata { - id: "abab6.5s-chat".to_string(), - display_name: "MiniMax abab6.5s".to_string(), - provider_id: "minimax".to_string(), - provider_name: "MiniMax".to_string(), - family: Some("abab".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(1.0), - output_per_million: Some(1.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(245760), - max_output_tokens: Some(8192), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("MiniMax 长上下文模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }] -} - -/// 零一万物 Yi 系列模型 -fn get_yi_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![ - EnhancedModelMetadata { - id: "yi-large".to_string(), - display_name: "Yi Large".to_string(), - provider_id: "yi".to_string(), - provider_name: "零一万物".to_string(), - family: Some("yi".to_string()), - tier: ModelTier::Max, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(20.0), - output_per_million: Some(20.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(32768), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-05-01".to_string()), - is_latest: true, - description: Some("零一万物旗舰模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "yi-medium".to_string(), - display_name: "Yi Medium".to_string(), - provider_id: "yi".to_string(), - provider_name: "零一万物".to_string(), - family: Some("yi".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(2.5), - output_per_million: Some(2.5), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(16384), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-05-01".to_string()), - is_latest: true, - description: Some("零一万物标准模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "yi-spark".to_string(), - display_name: "Yi Spark".to_string(), - provider_id: "yi".to_string(), - provider_name: "零一万物".to_string(), - family: Some("yi".to_string()), - tier: ModelTier::Mini, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(1.0), - output_per_million: Some(1.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(16384), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-05-01".to_string()), - is_latest: true, - description: Some("零一万物快速模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - ] -} - -/// 阶跃星辰 Step 系列模型 -fn get_stepfun_models() -> Vec { - let now = chrono::Utc::now().timestamp(); - vec![ - EnhancedModelMetadata { - id: "step-2-16k".to_string(), - display_name: "Step 2 16K".to_string(), - provider_id: "stepfun".to_string(), - provider_name: "阶跃星辰".to_string(), - family: Some("step".to_string()), - tier: ModelTier::Max, - capabilities: ModelCapabilities { - vision: true, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(38.0), - output_per_million: Some(120.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(16384), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-09-01".to_string()), - is_latest: true, - description: Some("阶跃星辰旗舰模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "step-1-128k".to_string(), - display_name: "Step 1 128K".to_string(), - provider_id: "stepfun".to_string(), - provider_name: "阶跃星辰".to_string(), - family: Some("step".to_string()), - tier: ModelTier::Pro, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(40.0), - output_per_million: Some(100.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(128000), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("阶跃星辰长上下文模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - EnhancedModelMetadata { - id: "step-1-flash".to_string(), - display_name: "Step 1 Flash".to_string(), - provider_id: "stepfun".to_string(), - provider_name: "阶跃星辰".to_string(), - family: Some("step".to_string()), - tier: ModelTier::Mini, - capabilities: ModelCapabilities { - vision: false, - tools: true, - streaming: true, - json_mode: true, - function_calling: true, - reasoning: false, - }, - pricing: Some(ModelPricing { - input_per_million: Some(1.0), - output_per_million: Some(4.0), - cache_read_per_million: None, - cache_write_per_million: None, - currency: "CNY".to_string(), - }), - limits: ModelLimits { - context_length: Some(8192), - max_output_tokens: Some(4096), - requests_per_minute: None, - tokens_per_minute: None, - }, - status: ModelStatus::Active, - release_date: Some("2024-06-01".to_string()), - is_latest: true, - description: Some("阶跃星辰快速模型".to_string()), - source: ModelSource::Local, - created_at: now, - updated_at: now, - }, - ] -} diff --git a/src-tauri/src/data/mod.rs b/src-tauri/src/data/mod.rs index 390169bb4..60a799247 100644 --- a/src-tauri/src/data/mod.rs +++ b/src-tauri/src/data/mod.rs @@ -1,7 +1,4 @@ //! 静态数据模块 //! -//! 包含本地硬编码的模型数据等 - -pub mod local_models; - -pub use local_models::get_local_models; +//! 模型数据现在从 aiclientproxy/models 仓库获取 +//! 本地硬编码数据已迁移到独立仓库: https://github.com/aiclientproxy/models diff --git a/src-tauri/src/server/handlers/api.rs b/src-tauri/src/server/handlers/api.rs index 29237ba56..f22c8e592 100644 --- a/src-tauri/src/server/handlers/api.rs +++ b/src-tauri/src/server/handlers/api.rs @@ -787,101 +787,144 @@ pub async fn chat_completions( let credential = if credential.is_none() { eprintln!("[CHAT_COMPLETIONS] Provider Pool 中未找到凭证,尝试 API Key Provider..."); - // 根据 selected_provider 映射到 ApiProviderType use crate::database::dao::api_key_provider::ApiProviderType; - let api_provider_type = match selected_provider.to_lowercase().as_str() { - "anthropic" | "claude" => Some(ApiProviderType::Anthropic), - "openai" => Some(ApiProviderType::Openai), - "gemini" => Some(ApiProviderType::Gemini), - // 以下都是 OpenAI 兼容的 Provider - "deepseek" | "moonshot" | "groq" | "grok" | "mistral" | "perplexity" | "cohere" - | "openrouter" | "silicon" => Some(ApiProviderType::Openai), - _ => None, - }; + let provider_id_lower = selected_provider.to_lowercase(); - if let (Some(db), Some(api_type)) = (&state.db, api_provider_type) { + // 策略 1: 优先按 provider_id 直接查找(支持 deepseek, moonshot 等 60+ Provider) + // 这些 Provider 在 API Key Provider 中有独立配置 + let mut found_credential: Option = + None; + + if let Some(db) = &state.db { + // 先尝试按 provider_id 直接查找 eprintln!( - "[CHAT_COMPLETIONS] 尝试从 API Key Provider 类型 '{:?}' 获取凭证", - api_type + "[CHAT_COMPLETIONS] 尝试按 provider_id '{}' 直接查找凭证", + provider_id_lower ); - // 使用按类型获取的方法(包括自定义 Provider) - match state.api_key_service.get_next_api_key_by_type(db, api_type) { - Ok(Some((_key_id, api_key, provider_info))) => { + match state.api_key_service.get_fallback_credential( + db, + &crate::models::provider_pool_model::PoolProviderType::OpenAI, + Some(&provider_id_lower), + ) { + Ok(Some(cred)) => { eprintln!( - "[CHAT_COMPLETIONS] 从 API Key Provider 获取到凭证: provider={}, api_host={}", - provider_info.name, - provider_info.api_host + "[CHAT_COMPLETIONS] 通过 provider_id '{}' 找到凭证: name={:?}", + provider_id_lower, cred.name ); - - let base_url = if provider_info.api_host.is_empty() { - None - } else { - Some(provider_info.api_host.clone()) - }; - - let provider_type = match provider_info.provider_type { - ApiProviderType::Anthropic => crate::ProviderType::Anthropic, - ApiProviderType::Openai | ApiProviderType::OpenaiResponse => { - crate::ProviderType::OpenAI - } - ApiProviderType::Gemini => crate::ProviderType::GeminiApiKey, - _ => crate::ProviderType::OpenAI, - }; - - // 根据 provider_type 创建对应的 CredentialData - let credential_data = match provider_type { - crate::ProviderType::Anthropic => { - crate::models::provider_pool_model::CredentialData::AnthropicKey { - api_key: api_key.clone(), - base_url, - } - } - crate::ProviderType::GeminiApiKey => { - crate::models::provider_pool_model::CredentialData::GeminiApiKey { - api_key: api_key.clone(), - base_url, - excluded_models: vec![], - } - } - _ => crate::models::provider_pool_model::CredentialData::OpenAIKey { - api_key: api_key.clone(), - base_url, - }, - }; - - // 构建 ProviderCredential - let mut cred = crate::models::provider_pool_model::ProviderCredential::new( - provider_type, - credential_data, - ); - cred.name = Some(provider_info.name.clone()); - state.logs.write().await.add( "info", &format!( - "[ROUTE] Using API Key Provider credential: provider={}, type={:?}", - provider_info.name, provider_info.provider_type + "[ROUTE] Using API Key Provider credential by provider_id: {}", + provider_id_lower ), ); - - Some(cred) + found_credential = Some(cred); } Ok(None) => { eprintln!( - "[CHAT_COMPLETIONS] API Key Provider 类型 '{:?}' 没有可用的 API Key", - api_type + "[CHAT_COMPLETIONS] provider_id '{}' 未找到凭证,尝试按类型查找", + provider_id_lower ); - None } Err(e) => { - eprintln!("[CHAT_COMPLETIONS] 从 API Key Provider 获取凭证失败: {}", e); - None + eprintln!("[CHAT_COMPLETIONS] 按 provider_id 查找凭证失败: {}", e); + } + } + + // 策略 2: 如果按 provider_id 未找到,按类型查找 + if found_credential.is_none() { + let api_provider_type = match provider_id_lower.as_str() { + "anthropic" | "claude" => Some(ApiProviderType::Anthropic), + "openai" => Some(ApiProviderType::Openai), + "gemini" => Some(ApiProviderType::Gemini), + // 以下都是 OpenAI 兼容的 Provider,但优先按 provider_id 查找已在上面处理 + "deepseek" | "moonshot" | "groq" | "grok" | "mistral" | "perplexity" + | "cohere" | "openrouter" | "silicon" => Some(ApiProviderType::Openai), + _ => None, + }; + + if let Some(api_type) = api_provider_type { + eprintln!( + "[CHAT_COMPLETIONS] 尝试从 API Key Provider 类型 '{:?}' 获取凭证", + api_type + ); + + match state.api_key_service.get_next_api_key_by_type(db, api_type) { + Ok(Some((_key_id, api_key, provider_info))) => { + eprintln!( + "[CHAT_COMPLETIONS] 从 API Key Provider 获取到凭证: provider={}, api_host={}", + provider_info.name, + provider_info.api_host + ); + + let base_url = if provider_info.api_host.is_empty() { + None + } else { + Some(provider_info.api_host.clone()) + }; + + let provider_type = match provider_info.provider_type { + ApiProviderType::Anthropic => crate::ProviderType::Anthropic, + ApiProviderType::Openai | ApiProviderType::OpenaiResponse => { + crate::ProviderType::OpenAI + } + ApiProviderType::Gemini => crate::ProviderType::GeminiApiKey, + _ => crate::ProviderType::OpenAI, + }; + + let credential_data = match provider_type { + crate::ProviderType::Anthropic => { + crate::models::provider_pool_model::CredentialData::AnthropicKey { + api_key: api_key.clone(), + base_url, + } + } + crate::ProviderType::GeminiApiKey => { + crate::models::provider_pool_model::CredentialData::GeminiApiKey { + api_key: api_key.clone(), + base_url, + excluded_models: vec![], + } + } + _ => crate::models::provider_pool_model::CredentialData::OpenAIKey { + api_key: api_key.clone(), + base_url, + }, + }; + + let mut cred = + crate::models::provider_pool_model::ProviderCredential::new( + provider_type, + credential_data, + ); + cred.name = Some(provider_info.name.clone()); + + state.logs.write().await.add( + "info", + &format!( + "[ROUTE] Using API Key Provider credential: provider={}, type={:?}", + provider_info.name, provider_info.provider_type + ), + ); + + found_credential = Some(cred); + } + Ok(None) => { + eprintln!( + "[CHAT_COMPLETIONS] API Key Provider 类型 '{:?}' 没有可用的 API Key", + api_type + ); + } + Err(e) => { + eprintln!("[CHAT_COMPLETIONS] 从 API Key Provider 获取凭证失败: {}", e); + } + } } } - } else { - None } + + found_credential } else { credential }; diff --git a/src-tauri/src/server/mod.rs b/src-tauri/src/server/mod.rs index 868dc3b0d..1790860f9 100644 --- a/src-tauri/src/server/mod.rs +++ b/src-tauri/src/server/mod.rs @@ -1467,8 +1467,18 @@ async fn amp_chat_completions( ); // 尝试根据 provider 名称选择凭证(带智能降级) + eprintln!( + "[AMP] 开始查找凭证: provider={}, model={}, db={}", + provider, + request.model, + state.db.is_some() + ); let credential = match &state.db { Some(db) => { + eprintln!( + "[AMP] 调用 select_credential_with_fallback, provider_id_hint={}", + provider + ); // 首先尝试按 provider 类型选择(带智能降级) if let Ok(Some(cred)) = state.pool_service.select_credential_with_fallback( db, @@ -1477,20 +1487,30 @@ async fn amp_chat_completions( Some(&request.model), Some(&provider), // provider_id_hint 使用路由中的 provider 名称 ) { + eprintln!( + "[AMP] select_credential_with_fallback 找到凭证: {:?}", + cred.name + ); Some(cred) } // 然后尝试按名称查找 else if let Ok(Some(cred)) = state.pool_service.get_by_name(db, &provider) { + eprintln!("[AMP] get_by_name 找到凭证: {:?}", cred.name); Some(cred) } // 最后尝试按 UUID 查找 else if let Ok(Some(cred)) = state.pool_service.get_by_uuid(db, &provider) { + eprintln!("[AMP] get_by_uuid 找到凭证: {:?}", cred.name); Some(cred) } else { + eprintln!("[AMP] 未找到任何凭证 for provider '{}'", provider); None } } - None => None, + None => { + eprintln!("[AMP] 数据库未初始化"); + None + } }; match credential { diff --git a/src-tauri/src/services/api_key_provider_service.rs b/src-tauri/src/services/api_key_provider_service.rs index e18c19c04..4133beb48 100644 --- a/src-tauri/src/services/api_key_provider_service.rs +++ b/src-tauri/src/services/api_key_provider_service.rs @@ -316,6 +316,8 @@ impl ApiKeyProviderService { // ==================== API Key 操作 ==================== /// 添加 API Key + /// + /// 当添加第一个 API Key 时,会自动启用 Provider pub fn add_api_key( &self, db: &DbConnection, @@ -325,10 +327,15 @@ impl ApiKeyProviderService { ) -> Result { // 验证 Provider 存在 let conn = db.lock().map_err(|e| e.to_string())?; - let _ = ApiKeyProviderDao::get_provider_by_id(&conn, provider_id) + let provider = ApiKeyProviderDao::get_provider_by_id(&conn, provider_id) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Provider not found: {}", provider_id))?; + // 检查是否是第一个 API Key,如果是则自动启用 Provider + let existing_keys = ApiKeyProviderDao::get_api_keys_by_provider(&conn, provider_id) + .map_err(|e| e.to_string())?; + let should_enable_provider = existing_keys.is_empty() && !provider.enabled; + // 加密 API Key let encrypted_key = self.encryption.encrypt(api_key); @@ -347,6 +354,19 @@ impl ApiKeyProviderService { ApiKeyProviderDao::insert_api_key(&conn, &key).map_err(|e| e.to_string())?; + // 如果是第一个 API Key,自动启用 Provider + if should_enable_provider { + let mut updated_provider = provider; + updated_provider.enabled = true; + updated_provider.updated_at = now; + ApiKeyProviderDao::update_provider(&conn, &updated_provider) + .map_err(|e| e.to_string())?; + tracing::info!( + "[ApiKeyProviderService] 自动启用 Provider: {} (添加了第一个 API Key)", + provider_id + ); + } + Ok(key) } @@ -696,26 +716,49 @@ impl ApiKeyProviderService { pool_type: &PoolProviderType, provider_id_hint: Option<&str>, ) -> Result, String> { - // 策略 1: 通过类型映射查找 - if let Some(api_type) = self.map_pool_type_to_api_type(pool_type) { - tracing::debug!("[智能降级] 尝试类型映射: {:?} -> {:?}", pool_type, api_type); - if let Some(cred) = self.find_by_api_type(db, pool_type, &api_type)? { - return Ok(Some(cred)); - } - } + eprintln!( + "[get_fallback_credential] 开始查找: pool_type={:?}, provider_id_hint={:?}", + pool_type, provider_id_hint + ); - // 策略 2: 通过 provider_id 直接查找 (支持 60+ Provider) + // 策略 1: 优先通过 provider_id 直接查找 (支持 deepseek, moonshot 等 60+ Provider) + // 这些 Provider 在 API Key Provider 中有独立配置,应该优先使用 if let Some(provider_id) = provider_id_hint { - tracing::debug!("[智能降级] 尝试 provider_id 查找: {}", provider_id); + eprintln!( + "[get_fallback_credential] 尝试按 provider_id '{}' 查找", + provider_id + ); if let Some(cred) = self.find_by_provider_id(db, provider_id)? { + eprintln!( + "[get_fallback_credential] 通过 provider_id '{}' 找到凭证: {:?}", + provider_id, cred.name + ); + return Ok(Some(cred)); + } + eprintln!( + "[get_fallback_credential] provider_id '{}' 未找到凭证", + provider_id + ); + } + + // 策略 2: 通过类型映射查找(降级方案) + if let Some(api_type) = self.map_pool_type_to_api_type(pool_type) { + eprintln!( + "[get_fallback_credential] 尝试类型映射: {:?} -> {:?}", + pool_type, api_type + ); + if let Some(cred) = self.find_by_api_type(db, pool_type, &api_type)? { + eprintln!( + "[get_fallback_credential] 通过类型映射找到凭证: {:?}", + cred.name + ); return Ok(Some(cred)); } } - tracing::debug!( - "[智能降级] 未找到 {:?} 的降级凭证 (provider_id_hint: {:?})", - pool_type, - provider_id_hint + eprintln!( + "[get_fallback_credential] 未找到 {:?} 的降级凭证 (provider_id_hint: {:?})", + pool_type, provider_id_hint ); Ok(None) } @@ -831,8 +874,24 @@ impl ApiKeyProviderService { ApiKeyProviderDao::get_provider_by_id(&conn, provider_id).map_err(|e| e.to_string())?; let provider = match provider { - Some(p) if p.enabled => p, - _ => return Ok(None), + Some(p) if p.enabled => { + eprintln!( + "[find_by_provider_id] 找到已启用的 provider: id={}, name={}, api_host={}", + p.id, p.name, p.api_host + ); + p + } + Some(_p) => { + eprintln!( + "[find_by_provider_id] provider '{}' 存在但未启用", + provider_id + ); + return Ok(None); + } + None => { + eprintln!("[find_by_provider_id] provider '{}' 不存在", provider_id); + return Ok(None); + } }; // 获取启用的 API Key @@ -840,9 +899,19 @@ impl ApiKeyProviderService { .map_err(|e| e.to_string())?; if keys.is_empty() { + eprintln!( + "[find_by_provider_id] provider '{}' 没有启用的 API Key", + provider_id + ); return Ok(None); } + eprintln!( + "[find_by_provider_id] provider '{}' 有 {} 个启用的 API Key", + provider_id, + keys.len() + ); + // 轮询选择 API Key let index = { let mut indices = self.round_robin_index.write().map_err(|e| e.to_string())?; diff --git a/src-tauri/src/services/model_registry_service.rs b/src-tauri/src/services/model_registry_service.rs index 4cad6a25c..d4a33520c 100644 --- a/src-tauri/src/services/model_registry_service.rs +++ b/src-tauri/src/services/model_registry_service.rs @@ -1,21 +1,90 @@ //! 模型注册服务 //! -//! 负责从 models.dev API 获取模型数据、管理本地缓存、提供模型搜索等功能 +//! 负责从 aiclientproxy/models 仓库获取模型数据、管理本地缓存、提供模型搜索等功能 -use crate::data::get_local_models; use crate::database::DbConnection; use crate::models::model_registry::{ - EnhancedModelMetadata, ModelSource, ModelStatus, ModelSyncState, ModelTier, ModelsDevProvider, - UserModelPreference, + EnhancedModelMetadata, ModelCapabilities, ModelLimits, ModelPricing, ModelSource, ModelStatus, + ModelSyncState, ModelTier, UserModelPreference, }; use rusqlite::params; -use std::collections::HashMap; +use serde::Deserialize; use std::sync::Arc; use tokio::sync::RwLock; -const MODELS_DEV_API_URL: &str = "https://models.dev/api.json"; +/// GitHub 仓库 raw 文件基础 URL +const MODELS_REPO_BASE_URL: &str = "https://raw.githubusercontent.com/aiclientproxy/models/main"; const CACHE_DURATION_SECS: i64 = 3600; // 1 小时 +/// 仓库索引文件结构 +#[derive(Debug, Deserialize)] +struct RepoIndex { + providers: Vec, + #[allow(dead_code)] + total_models: u32, +} + +/// 仓库中的 Provider 数据结构 +#[derive(Debug, Deserialize)] +struct RepoProviderData { + provider: RepoProvider, + models: Vec, +} + +#[derive(Debug, Deserialize)] +struct RepoProvider { + id: String, + name: String, +} + +#[derive(Debug, Deserialize)] +struct RepoModel { + id: String, + name: String, + family: Option, + tier: Option, + capabilities: Option, + pricing: Option, + limits: Option, + status: Option, + release_date: Option, + is_latest: Option, + description: Option, + #[serde(default)] + description_zh: Option, +} + +#[derive(Debug, Deserialize, Default)] +struct RepoCapabilities { + #[serde(default)] + vision: bool, + #[serde(default)] + tools: bool, + #[serde(default)] + streaming: bool, + #[serde(default)] + json_mode: bool, + #[serde(default)] + function_calling: bool, + #[serde(default)] + reasoning: bool, +} + +#[derive(Debug, Deserialize)] +struct RepoPricing { + input: Option, + output: Option, + cache_read: Option, + cache_write: Option, + currency: Option, +} + +#[derive(Debug, Deserialize)] +struct RepoLimits { + context: Option, + max_output: Option, +} + /// 模型注册服务 pub struct ModelRegistryService { /// 数据库连接 @@ -62,24 +131,7 @@ impl ModelRegistryService { } } - // 2. 使用本地硬编码数据作为初始数据 - let local_models = get_local_models(); - tracing::info!( - "[ModelRegistry] 使用 {} 个本地硬编码模型作为初始数据", - local_models.len() - ); - - { - let mut cache = self.models_cache.write().await; - *cache = local_models.clone(); - } - - // 保存到数据库 - if let Err(e) = self.save_models_to_db(&local_models).await { - tracing::warn!("[ModelRegistry] 保存本地模型到数据库失败: {}", e); - } - - // 3. 后台获取 models.dev 数据 + // 2. 后台获取 models 仓库数据 self.spawn_background_refresh(); Ok(()) @@ -109,15 +161,15 @@ impl ModelRegistryService { models_cache, sync_state, }; - if let Err(e) = service.refresh_from_models_dev().await { + if let Err(e) = service.refresh_from_repo().await { tracing::error!("[ModelRegistry] 后台刷新失败: {}", e); } }); } - /// 从 models.dev API 刷新数据 - pub async fn refresh_from_models_dev(&self) -> Result<(), String> { - tracing::info!("[ModelRegistry] 开始从 models.dev 获取数据"); + /// 从 aiclientproxy/models 仓库刷新数据 + pub async fn refresh_from_repo(&self) -> Result<(), String> { + tracing::info!("[ModelRegistry] 开始从 models 仓库获取数据"); // 设置同步状态 { @@ -127,31 +179,27 @@ impl ModelRegistryService { } // 获取数据 - let result = self.fetch_models_dev_data().await; + let result = self.fetch_models_from_repo().await; match result { - Ok(models_dev_models) => { - // 合并本地模型 - let local_models = get_local_models(); - let merged = self.merge_models(models_dev_models, local_models); - - tracing::info!("[ModelRegistry] 获取并合并了 {} 个模型", merged.len()); + Ok(models) => { + tracing::info!("[ModelRegistry] 获取了 {} 个模型", models.len()); // 更新缓存 { let mut cache = self.models_cache.write().await; - *cache = merged.clone(); + *cache = models.clone(); } // 保存到数据库 - self.save_models_to_db(&merged).await?; + self.save_models_to_db(&models).await?; // 更新同步状态 { let mut state = self.sync_state.write().await; state.is_syncing = false; state.last_sync_at = Some(chrono::Utc::now().timestamp()); - state.model_count = merged.len() as u32; + state.model_count = models.len() as u32; state.last_error = None; } @@ -161,7 +209,7 @@ impl ModelRegistryService { Ok(()) } Err(e) => { - tracing::error!("[ModelRegistry] 从 models.dev 获取数据失败: {}", e); + tracing::error!("[ModelRegistry] 从 models 仓库获取数据失败: {}", e); // 更新同步状态 { @@ -175,76 +223,136 @@ impl ModelRegistryService { } } - /// 从 models.dev API 获取数据 - async fn fetch_models_dev_data(&self) -> Result, String> { + /// 从 models 仓库获取数据 + async fn fetch_models_from_repo(&self) -> Result, String> { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; - let response = client - .get(MODELS_DEV_API_URL) + // 1. 获取索引文件 + let index_url = format!("{}/index.json", MODELS_REPO_BASE_URL); + let index: RepoIndex = client + .get(&index_url) .header("User-Agent", "ProxyCast/1.0") .send() .await - .map_err(|e| format!("请求 models.dev 失败: {}", e))?; - - if !response.status().is_success() { - return Err(format!("models.dev 返回错误状态码: {}", response.status())); - } - - let data: HashMap = response + .map_err(|e| format!("请求 index.json 失败: {}", e))? .json() .await - .map_err(|e| format!("解析 models.dev 响应失败: {}", e))?; + .map_err(|e| format!("解析 index.json 失败: {}", e))?; - // 转换为内部格式 + tracing::info!( + "[ModelRegistry] 索引包含 {} 个 providers", + index.providers.len() + ); + + // 2. 并发获取所有 provider 数据 let mut models = Vec::new(); - for (provider_id, provider) in data { - for (_, model) in provider.models { - let enhanced = model.to_enhanced_metadata(&provider_id, &provider.name); - models.push(enhanced); + let now = chrono::Utc::now().timestamp(); + + for provider_id in &index.providers { + let provider_url = format!("{}/providers/{}.json", MODELS_REPO_BASE_URL, provider_id); + + match client + .get(&provider_url) + .header("User-Agent", "ProxyCast/1.0") + .send() + .await + { + Ok(response) => { + if response.status().is_success() { + match response.json::().await { + Ok(provider_data) => { + for model in provider_data.models { + let enhanced = self.convert_repo_model( + model, + &provider_data.provider.id, + &provider_data.provider.name, + now, + ); + models.push(enhanced); + } + } + Err(e) => { + tracing::warn!("[ModelRegistry] 解析 {} 失败: {}", provider_id, e); + } + } + } + } + Err(e) => { + tracing::warn!("[ModelRegistry] 获取 {} 失败: {}", provider_id, e); + } } } + // 按 provider_id 和 display_name 排序 + models.sort_by(|a, b| { + a.provider_id + .cmp(&b.provider_id) + .then(a.display_name.cmp(&b.display_name)) + }); + tracing::info!( - "[ModelRegistry] 从 models.dev 获取了 {} 个模型", + "[ModelRegistry] 从 models 仓库获取了 {} 个模型", models.len() ); Ok(models) } - /// 合并 models.dev 数据和本地数据 - fn merge_models( + /// 转换仓库模型格式为内部格式 + fn convert_repo_model( &self, - models_dev: Vec, - local: Vec, - ) -> Vec { - let mut merged: HashMap = HashMap::new(); + model: RepoModel, + provider_id: &str, + provider_name: &str, + now: i64, + ) -> EnhancedModelMetadata { + let caps = model.capabilities.unwrap_or_default(); - // 先添加 models.dev 数据 - for model in models_dev { - merged.insert(model.id.clone(), model); + EnhancedModelMetadata { + id: model.id, + display_name: model.name, + provider_id: provider_id.to_string(), + provider_name: provider_name.to_string(), + family: model.family, + tier: model + .tier + .and_then(|t| t.parse().ok()) + .unwrap_or(ModelTier::Pro), + capabilities: ModelCapabilities { + vision: caps.vision, + tools: caps.tools, + streaming: caps.streaming, + json_mode: caps.json_mode, + function_calling: caps.function_calling, + reasoning: caps.reasoning, + }, + pricing: model.pricing.map(|p| ModelPricing { + input_per_million: p.input, + output_per_million: p.output, + cache_read_per_million: p.cache_read, + cache_write_per_million: p.cache_write, + currency: p.currency.unwrap_or_else(|| "USD".to_string()), + }), + limits: ModelLimits { + context_length: model.limits.as_ref().and_then(|l| l.context), + max_output_tokens: model.limits.as_ref().and_then(|l| l.max_output), + requests_per_minute: None, + tokens_per_minute: None, + }, + status: model + .status + .and_then(|s| s.parse().ok()) + .unwrap_or(ModelStatus::Active), + release_date: model.release_date, + is_latest: model.is_latest.unwrap_or(false), + description: model.description_zh.or(model.description), + source: ModelSource::ModelsDev, + created_at: now, + updated_at: now, } - - // 本地数据覆盖或补充 - for model in local { - // 如果 models.dev 没有这个模型,或者本地数据更新,则使用本地数据 - if !merged.contains_key(&model.id) { - merged.insert(model.id.clone(), model); - } - } - - let mut result: Vec<_> = merged.into_values().collect(); - // 按 provider_id 和 display_name 排序 - result.sort_by(|a, b| { - a.provider_id - .cmp(&b.provider_id) - .then(a.display_name.cmp(&b.display_name)) - }); - - result } /// 从数据库加载模型 diff --git a/src-tauri/src/services/provider_pool_service.rs b/src-tauri/src/services/provider_pool_service.rs index 4e4fe2dfd..c79576998 100644 --- a/src-tauri/src/services/provider_pool_service.rs +++ b/src-tauri/src/services/provider_pool_service.rs @@ -223,7 +223,18 @@ impl ProviderPoolService { provider_type: &str, model: Option<&str>, ) -> Result, String> { - let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; + // 对于未知的 provider_type,直接返回 None(不是错误) + // 这样可以让 select_credential_with_fallback 继续尝试智能降级 + let pt: PoolProviderType = match provider_type.parse() { + Ok(pt) => pt, + Err(_) => { + eprintln!( + "[SELECT_CREDENTIAL] 未知的 provider_type '{}', 返回 None 以便智能降级", + provider_type + ); + return Ok(None); + } + }; let conn = db.lock().map_err(|e| e.to_string())?; // 获取凭证,对于 Anthropic 类型,也查找 Claude 类型的凭证 @@ -340,34 +351,42 @@ impl ProviderPoolService { model: Option<&str>, provider_id_hint: Option<&str>, ) -> Result, String> { + eprintln!( + "[select_credential_with_fallback] 开始: provider_type={}, model={:?}, provider_id_hint={:?}", + provider_type, model, provider_id_hint + ); + // Step 1: 尝试从 Provider Pool 选择 (OAuth + API Key) if let Some(cred) = self.select_credential(db, provider_type, model)? { - tracing::debug!( - "[凭证选择] 从 Provider Pool 找到 '{}' 凭证: {:?}", - provider_type, + eprintln!( + "[select_credential_with_fallback] 从 Provider Pool 找到凭证: {:?}", + cred.name + ); + return Ok(Some(cred)); + } + eprintln!("[select_credential_with_fallback] Provider Pool 未找到凭证,尝试智能降级"); + + // Step 2: 智能降级到 API Key Provider + let pt: PoolProviderType = provider_type.parse().unwrap_or(PoolProviderType::OpenAI); + eprintln!( + "[select_credential_with_fallback] 解析 provider_type '{}' -> {:?}", + provider_type, pt + ); + + // 传入 provider_id_hint 支持 60+ Provider + eprintln!("[select_credential_with_fallback] 调用 get_fallback_credential"); + if let Some(cred) = api_key_service.get_fallback_credential(db, &pt, provider_id_hint)? { + eprintln!( + "[select_credential_with_fallback] 智能降级成功: {:?}", cred.name ); return Ok(Some(cred)); } - // Step 2: 智能降级到 API Key Provider - let pt: PoolProviderType = provider_type.parse().unwrap_or(PoolProviderType::OpenAI); - - // 传入 provider_id_hint 支持 60+ Provider - if let Some(cred) = api_key_service.get_fallback_credential(db, &pt, provider_id_hint)? { - tracing::info!( - "[智能降级] Provider Pool 无 '{}' 凭证,使用 API Key Provider 降级 (provider_id: {:?})", - provider_type, - provider_id_hint - ); - return Ok(Some(cred)); - } - // Step 3: 都没有找到 - tracing::warn!( - "[凭证选择] 未找到 '{}' 的任何可用凭证 (provider_id_hint: {:?})", - provider_type, - provider_id_hint + eprintln!( + "[select_credential_with_fallback] 未找到任何凭证 for provider_type='{}'", + provider_type ); Ok(None) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e001a7717..efd998500 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.31.0", + "version": "0.32.0", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/agent/chat/components/ChatNavbar.tsx b/src/components/agent/chat/components/ChatNavbar.tsx index 522ab1a5d..7116b0e26 100644 --- a/src/components/agent/chat/components/ChatNavbar.tsx +++ b/src/components/agent/chat/components/ChatNavbar.tsx @@ -14,34 +14,50 @@ import { useApiKeyProvider } from "@/hooks/useApiKeyProvider"; import { useModelRegistry } from "@/hooks/useModelRegistry"; import { getDefaultProvider } from "@/hooks/useTauri"; -// OAuth 凭证类型到显示名称和 registry ID 的映射 -const CREDENTIAL_TYPE_CONFIG: Record< - string, - { label: string; registryId: string } -> = { - kiro: { label: "Kiro", registryId: "anthropic" }, - gemini: { label: "Gemini", registryId: "google" }, - qwen: { label: "通义千问", registryId: "alibaba" }, - antigravity: { label: "Antigravity", registryId: "google" }, - codex: { label: "Codex", registryId: "openai" }, - claude_oauth: { label: "Claude OAuth", registryId: "anthropic" }, - iflow: { label: "iFlow", registryId: "custom" }, - openai: { label: "OpenAI", registryId: "openai" }, - claude: { label: "Claude", registryId: "anthropic" }, - gemini_api_key: { label: "Gemini", registryId: "google" }, +// Provider type 到 registry ID 的映射(用于获取模型列表) +const getRegistryIdFromType = (providerType: string): string => { + const typeMap: Record = { + openai: "openai", + anthropic: "anthropic", + gemini: "google", + "azure-openai": "openai", + vertexai: "google", + ollama: "ollama", + kiro: "anthropic", + claude: "anthropic", + claude_oauth: "anthropic", + qwen: "alibaba", + codex: "openai", + antigravity: "google", + iflow: "openai", + gemini_api_key: "google", + }; + return typeMap[providerType.toLowerCase()] || providerType.toLowerCase(); }; -// API Key Provider 类型到显示名称和 registry ID 的映射 -const API_KEY_PROVIDER_CONFIG: Record< - string, - { label: string; registryId: string } -> = { - anthropic: { label: "Anthropic", registryId: "anthropic" }, - openai: { label: "OpenAI", registryId: "openai" }, - gemini: { label: "Gemini", registryId: "google" }, - "azure-openai": { label: "Azure OpenAI", registryId: "openai" }, - vertexai: { label: "VertexAI", registryId: "google" }, - ollama: { label: "Ollama", registryId: "ollama" }, +// 生成 Provider 的显示标签 +const getProviderLabel = (providerType: string): string => { + const labelMap: Record = { + kiro: "Kiro", + gemini: "Gemini", + qwen: "通义千问", + antigravity: "Antigravity", + codex: "Codex", + claude_oauth: "Claude OAuth", + claude: "Claude", + openai: "OpenAI", + anthropic: "Anthropic", + "azure-openai": "Azure OpenAI", + vertexai: "VertexAI", + ollama: "Ollama", + gemini_api_key: "Gemini", + iflow: "iFlow", + }; + // 如果在映射表中,使用映射;否则首字母大写 + return ( + labelMap[providerType.toLowerCase()] || + providerType.charAt(0).toUpperCase() + providerType.slice(1) + ); }; /** 已配置的 Provider 信息 */ @@ -49,6 +65,8 @@ interface ConfiguredProvider { key: string; label: string; registryId: string; + fallbackRegistryId?: string; // 当 registryId 没有模型时的回退 + type: string; // 原始 provider type,用于确定 API 协议 } interface ChatNavbarProps { @@ -79,7 +97,6 @@ export const ChatNavbar: React.FC = ({ // 用于防止无限循环 const hasInitialized = useRef(false); - const prevProviderType = useRef(providerType); // 获取凭证池数据 const { overview: oauthCredentials } = useProviderPool(); @@ -101,34 +118,40 @@ export const ChatNavbar: React.FC = ({ // 获取模型注册表数据 const { models: registryModels } = useModelRegistry({ autoLoad: true }); - // 计算已配置的 Provider 列表 + // 计算已配置的 Provider 列表(完全动态,无白名单限制) const configuredProviders = useMemo(() => { const providerMap = new Map(); - // 从 OAuth 凭证提取 Provider + // 从 OAuth 凭证提取 Provider(动态,支持所有类型) oauthCredentials.forEach((overview) => { if (overview.credentials.length > 0) { - const config = CREDENTIAL_TYPE_CONFIG[overview.provider_type]; - if (config && !providerMap.has(overview.provider_type)) { - providerMap.set(overview.provider_type, { - key: overview.provider_type, - label: config.label, - registryId: config.registryId, + const key = overview.provider_type; + if (!providerMap.has(key)) { + providerMap.set(key, { + key, + label: getProviderLabel(key), + registryId: getRegistryIdFromType(key), + type: key, }); } } }); - // 从 API Key Provider 提取(只包含有 API Key 的) + // 从 API Key Provider 提取(动态,支持所有自定义 Provider) + // 使用 provider.id 作为 key,确保每个 Provider 单独显示 apiKeyProviders .filter((p) => p.api_key_count > 0 && p.enabled) .forEach((provider) => { - const config = API_KEY_PROVIDER_CONFIG[provider.type]; - if (config && !providerMap.has(provider.type)) { - providerMap.set(provider.type, { - key: provider.type, - label: config.label, - registryId: config.registryId, + const key = provider.id; // 使用 provider.id 而不是 type 映射 + if (!providerMap.has(key)) { + // 优先使用 provider.id 作为 registryId(适用于系统预设的 Provider,如 deepseek, moonshot) + // 如果模型注册表中没有该 id 的模型,则回退到使用 type 映射(适用于自定义 Provider) + providerMap.set(key, { + key, + label: provider.name, // 使用 Provider 的 name 作为显示名称 + registryId: provider.id, // 先尝试用 id + fallbackRegistryId: getRegistryIdFromType(provider.type), // 回退用 type + type: provider.type, }); } }); @@ -142,13 +165,52 @@ export const ChatNavbar: React.FC = ({ }, [configuredProviders, providerType]); // 获取当前 Provider 的模型列表(从 model_registry 获取) + // 按照模型版本排序,最新的在前面 const currentModels = useMemo(() => { if (!selectedProvider) return []; // 从 model_registry 获取模型 - return registryModels + // 优先使用 registryId,如果没有模型则回退到 fallbackRegistryId + let models = registryModels .filter((m) => m.provider_id === selectedProvider.registryId) .map((m) => m.id); + + // 如果没有找到模型,尝试使用 fallbackRegistryId + if (models.length === 0 && selectedProvider.fallbackRegistryId) { + models = registryModels + .filter((m) => m.provider_id === selectedProvider.fallbackRegistryId) + .map((m) => m.id); + } + + // 按照模型名称排序,优先显示最新版本 + // 排序规则: + // 1. 带日期后缀的模型(如 claude-opus-4-5-20251101)按日期降序 + // 2. 带 "latest" 后缀的模型排在最前面 + // 3. 其他模型按字母顺序 + return models.sort((a, b) => { + const aIsLatest = a.includes("-latest"); + const bIsLatest = b.includes("-latest"); + + // latest 版本排在最前面 + if (aIsLatest && !bIsLatest) return -1; + if (!aIsLatest && bIsLatest) return 1; + + // 提取日期后缀(如 20251101) + const dateRegex = /-(\d{8})$/; + const aMatch = a.match(dateRegex); + const bMatch = b.match(dateRegex); + + if (aMatch && bMatch) { + // 两个都有日期,按日期降序(最新的在前) + return bMatch[1].localeCompare(aMatch[1]); + } + + if (aMatch && !bMatch) return -1; // 有日期的排在前面 + if (!aMatch && bMatch) return 1; + + // 其他情况按字母降序(通常版本号大的在前) + return b.localeCompare(a); + }); }, [selectedProvider, registryModels]); // 初始化:优先选择服务器默认 Provider,否则选择第一个已配置的 @@ -183,16 +245,16 @@ export const ChatNavbar: React.FC = ({ providerType, ]); - // 当 Provider 切换时,自动选择第一个模型 + // 当 Provider 切换或模型列表变化时,自动选择第一个模型 useEffect(() => { - // 只在 Provider 真正变化时触发 - if (providerType === prevProviderType.current) return; - prevProviderType.current = providerType; - - if (currentModels.length > 0 && !currentModels.includes(model)) { + // 如果模型列表不为空,且当前模型为空或不在列表中,选择第一个模型 + if ( + currentModels.length > 0 && + (!model || !currentModels.includes(model)) + ) { setModel(currentModels[0]); } - }, [providerType, currentModels, model, setModel]); + }, [currentModels, model, setModel]); const selectedProviderLabel = selectedProvider?.label || providerType; diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 08deecf35..30f78c806 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -227,6 +227,19 @@ export function ApiServerPage() { ollama: "ollama", }; + // 根据 Provider type 获取图标类型(用于自定义 Provider) + const getIconTypeFromProviderType = (providerType: string): string => { + const typeIconMap: Record = { + openai: "openai", + anthropic: "claude", + gemini: "gemini", + "azure-openai": "openai", + vertexai: "gemini", + ollama: "ollama", + }; + return typeIconMap[providerType.toLowerCase()] || "openai"; + }; + const [poolOverview, setPoolOverview] = useState([]); const [apiKeyProviders, setApiKeyProviders] = useState< ProviderWithKeysDisplay[] @@ -292,11 +305,12 @@ export function ApiServerPage() { }); // 添加 API Key Provider 中有 API Key 的 Provider + // 使用 provider.id 作为 key,确保每个 Provider 单独显示 apiKeyProviders.forEach((provider) => { const enabledKeys = provider.api_keys.filter((k) => k.enabled); if (enabledKeys.length > 0 && provider.enabled) { - // 将 API Key Provider 类型映射到统一的 ID - const id = mapApiKeyProviderToId(provider.type); + // 使用 provider.id 而不是 type 映射,确保自定义 Provider 单独显示 + const id = provider.id; const existing = providerMap.get(id); if (existing) { existing.apiKeyCount = enabledKeys.length; @@ -306,10 +320,15 @@ export function ApiServerPage() { ? "both" : "api_key"; } else { + // 根据 provider.type 确定图标类型(优先使用 id 映射,否则使用 type 映射) + const iconType = + providerIconMap[id] || + providerIconMap[provider.type] || + getIconTypeFromProviderType(provider.type); providerMap.set(id, { id, label: providerLabels[id] || provider.name, - iconType: providerIconMap[id] || "openai", + iconType, source: "api_key", oauthCount: 0, apiKeyCount: enabledKeys.length, @@ -803,7 +822,15 @@ export function ApiServerPage() { ).filter((cred) => !cred.is_disabled); // 获取 API Key 凭证 - 查找所有映射到当前 defaultProvider 的 API Key Provider + // 支持两种匹配方式: + // 1. 通过 provider.id 直接匹配(用于自定义 Provider) + // 2. 通过 type 映射匹配(用于内置 Provider) const matchingApiKeyProviders = apiKeyProviders.filter((p) => { + // 首先尝试直接通过 id 匹配 + if (p.id === defaultProvider && p.enabled) { + return true; + } + // 然后尝试通过 type 映射匹配 const mappedId = mapApiKeyProviderToId(p.type); return mappedId === defaultProvider && p.enabled; }); diff --git a/src/components/api-server/ModelsTab.tsx b/src/components/api-server/ModelsTab.tsx index 05dbfcc57..d1f0cbddc 100644 --- a/src/components/api-server/ModelsTab.tsx +++ b/src/components/api-server/ModelsTab.tsx @@ -1,60 +1,49 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Cpu, RefreshCw, Copy, Check, Search } from "lucide-react"; import { getAvailableModels, ModelInfo } from "@/hooks/useTauri"; -// 模型分组配置 -const MODEL_GROUPS: Record< - string, - { name: string; color: string; models: string[] } -> = { - kiro: { - name: "Kiro Claude", - color: "bg-purple-100 text-purple-700", - models: [ - "claude-sonnet-4-5", - "claude-sonnet-4-5-20250514", - "claude-sonnet-4-5-20250929", - "claude-3-7-sonnet-20250219", - "claude-3-5-sonnet-latest", - "claude-3-5-sonnet-20241022", - "claude-opus-4-5-20250514", - "claude-haiku-4-5-20250514", - ], +// 根据 provider_id 获取分组配置 +const PROVIDER_GROUPS: Record = { + anthropic: { + name: "Anthropic", + color: + "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300", }, - gemini: { - name: "Gemini CLI", - color: "bg-blue-100 text-blue-700", - models: [ - "gemini-2.5-flash", - "gemini-2.5-flash-lite", - "gemini-2.5-pro", - "gemini-2.5-pro-preview-06-05", - "gemini-3-pro-preview", - "gemini-2.0-flash-exp", - ], - }, - qwen: { - name: "通义千问", - color: "bg-orange-100 text-orange-700", - models: [ - "qwen3-coder-plus", - "qwen3-coder-flash", - "qwen-coder-plus", - "qwen-coder-turbo", - ], + google: { + name: "Google", + color: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300", }, openai: { name: "OpenAI", - color: "bg-green-100 text-green-700", - models: [ - "gpt-4o", - "gpt-4o-mini", - "gpt-4-turbo", - "gpt-4", - "gpt-3.5-turbo", - "o1-preview", - "o1-mini", - ], + color: + "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300", + }, + dashscope: { + name: "阿里云", + color: + "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300", + }, + deepseek: { + name: "DeepSeek", + color: "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300", + }, + zhipu: { + name: "智谱", + color: + "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300", + }, + moonshot: { + name: "月之暗面", + color: + "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300", + }, + mistral: { + name: "Mistral", + color: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300", + }, + cohere: { + name: "Cohere", + color: "bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300", }, }; @@ -64,7 +53,7 @@ export function ModelsTab() { const [error, setError] = useState(null); const [copied, setCopied] = useState(null); const [search, setSearch] = useState(""); - const [selectedGroup, setSelectedGroup] = useState(null); + const [selectedProvider, setSelectedProvider] = useState(null); useEffect(() => { fetchModels(); @@ -91,61 +80,59 @@ export function ModelsTab() { setTimeout(() => setCopied(null), 2000); }; - const getModelGroup = (modelId: string): string | null => { - for (const [groupId, group] of Object.entries(MODEL_GROUPS)) { - if ( - group.models.some((m) => - modelId.toLowerCase().includes(m.toLowerCase().split("-")[0]), - ) - ) { - return groupId; - } + const getProviderBadge = (providerId: string) => { + const config = PROVIDER_GROUPS[providerId]; + if (!config) { + return ( + + {providerId} + + ); } - // 根据 owned_by 判断 - const model = models.find((m) => m.id === modelId); - if (model?.owned_by === "anthropic") return "kiro"; - if (model?.owned_by === "google") return "gemini"; - if (model?.owned_by === "alibaba") return "qwen"; - if (model?.owned_by === "openai") return "openai"; - return null; - }; - - const getGroupBadge = (groupId: string | null) => { - if (!groupId || !MODEL_GROUPS[groupId]) return null; - const group = MODEL_GROUPS[groupId]; return ( - {group.name} + {config.name} ); }; - // 过滤模型 - const filteredModels = models.filter((model) => { - const matchesSearch = model.id.toLowerCase().includes(search.toLowerCase()); - const matchesGroup = - !selectedGroup || getModelGroup(model.id) === selectedGroup; - return matchesSearch && matchesGroup; - }); - // 按 provider 分组统计 - const groupCounts = models.reduce( - (acc, model) => { - const group = getModelGroup(model.id); - if (group) { - acc[group] = (acc[group] || 0) + 1; - } - return acc; - }, - {} as Record, - ); + const providerCounts = useMemo(() => { + return models.reduce( + (acc, model) => { + const provider = model.owned_by; + acc[provider] = (acc[provider] || 0) + 1; + return acc; + }, + {} as Record, + ); + }, [models]); + + // 获取所有 provider 列表(按数量排序) + const providers = useMemo(() => { + return Object.entries(providerCounts) + .sort((a, b) => b[1] - a[1]) + .map(([id]) => id); + }, [providerCounts]); + + // 过滤模型 + const filteredModels = useMemo(() => { + return models.filter((model) => { + const matchesSearch = model.id + .toLowerCase() + .includes(search.toLowerCase()); + const matchesProvider = + !selectedProvider || model.owned_by === selectedProvider; + return matchesSearch && matchesProvider; + }); + }, [models, search, selectedProvider]); return (
{error && ( -
+
{error}
)} @@ -175,31 +162,33 @@ export function ModelsTab() { {/* Provider 过滤标签 */}
- {Object.entries(MODEL_GROUPS).map(([groupId, group]) => { - const count = groupCounts[groupId] || 0; - if (count === 0) return null; + {providers.map((providerId) => { + const count = providerCounts[providerId] || 0; + const config = PROVIDER_GROUPS[providerId]; return ( ); })} @@ -237,7 +226,7 @@ export function ModelsTab() {
{model.id} - {getGroupBadge(getModelGroup(model.id))} + {getProviderBadge(model.owned_by)}

{model.owned_by} diff --git a/src/components/routing/ModelMapping.tsx b/src/components/routing/ModelMapping.tsx deleted file mode 100644 index 0ae4a65c4..000000000 --- a/src/components/routing/ModelMapping.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useState } from "react"; -import { Plus, Trash2, ArrowRight, Check, X } from "lucide-react"; -import type { ModelAlias } from "@/lib/api/router"; - -interface ModelMappingProps { - aliases: ModelAlias[]; - onAdd: (alias: string, actual: string) => Promise; - onRemove: (alias: string) => Promise; - loading?: boolean; -} - -export function ModelMapping({ - aliases, - onAdd, - onRemove, - loading, -}: ModelMappingProps) { - const [isAdding, setIsAdding] = useState(false); - const [newAlias, setNewAlias] = useState(""); - const [newActual, setNewActual] = useState(""); - const [addError, setAddError] = useState(null); - const [deletingAlias, setDeletingAlias] = useState(null); - - const handleAdd = async () => { - if (!newAlias.trim() || !newActual.trim()) { - setAddError("别名和实际模型名都不能为空"); - return; - } - - // Check for duplicate alias - if (aliases.some((a) => a.alias === newAlias.trim())) { - setAddError("该别名已存在"); - return; - } - - try { - await onAdd(newAlias.trim(), newActual.trim()); - setNewAlias(""); - setNewActual(""); - setIsAdding(false); - setAddError(null); - } catch (e) { - setAddError(e instanceof Error ? e.message : String(e)); - } - }; - - const handleRemove = async (alias: string) => { - setDeletingAlias(alias); - try { - await onRemove(alias); - } finally { - setDeletingAlias(null); - } - }; - - const handleCancel = () => { - setIsAdding(false); - setNewAlias(""); - setNewActual(""); - setAddError(null); - }; - - return ( -

-
-
-

模型别名映射

-

- 定义模型别名,使用熟悉的名称映射到实际模型 -

-
- {!isAdding && ( - - )} -
- - {/* Add new alias form */} - {isAdding && ( -
-
-
- - setNewAlias(e.target.value)} - placeholder="例如: gpt-4" - className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none" - autoFocus - /> -
- -
- - setNewActual(e.target.value)} - placeholder="例如: claude-sonnet-4-5-20250514" - className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none" - /> -
-
- {addError &&

{addError}

} -
- - -
-
- )} - - {/* Aliases list */} - {aliases.length === 0 && !isAdding ? ( -
-

暂无模型别名

-

点击"添加别名"创建第一个映射

-
- ) : ( -
- {aliases.map((alias) => ( -
-
- - {alias.alias} - - - - {alias.actual} - -
- -
- ))} -
- )} -
- ); -} diff --git a/src/components/routing/RoutingPage.tsx b/src/components/routing/RoutingPage.tsx index 8920d6328..ce2dc06e6 100644 --- a/src/components/routing/RoutingPage.tsx +++ b/src/components/routing/RoutingPage.tsx @@ -1,7 +1,5 @@ import { useState, useEffect, forwardRef, useImperativeHandle } from "react"; -import { RefreshCw, Route, Sparkles, Check, Trash2 } from "lucide-react"; -import { Modal } from "@/components/Modal"; -import { ModelMapping } from "./ModelMapping"; +import { RefreshCw, Route, Trash2 } from "lucide-react"; import { RoutingRules } from "./RoutingRules"; import { ExclusionList } from "./ExclusionList"; import { InjectionRules } from "./InjectionRules"; @@ -9,20 +7,14 @@ import { ClientRouting } from "./ClientRouting"; import { HelpTip } from "@/components/HelpTip"; import { routerApi } from "@/lib/api/router"; import { injectionApi } from "@/lib/api/injection"; -import { setEndpointProvider } from "@/hooks/useTauri"; -import type { - ModelAlias, - RoutingRule, - ProviderType, - RecommendedPreset, -} from "@/lib/api/router"; +import type { RoutingRule, ProviderType } from "@/lib/api/router"; import type { InjectionRule } from "@/lib/api/injection"; export interface RoutingPageRef { refresh: () => void; } -type TabType = "aliases" | "rules" | "exclusions" | "injection" | "clients"; +type TabType = "rules" | "exclusions" | "injection" | "clients"; interface RoutingPageProps { hideHeader?: boolean; @@ -35,7 +27,6 @@ export const RoutingPage = forwardRef( const [error, setError] = useState(null); // Data state - const [aliases, setAliases] = useState([]); const [rules, setRules] = useState([]); const [exclusions, setExclusions] = useState< Record @@ -43,34 +34,19 @@ export const RoutingPage = forwardRef( const [injectionRules, setInjectionRules] = useState([]); const [injectionEnabled, setInjectionEnabled] = useState(false); - // Presets state - const [presets, setPresets] = useState([]); - const [showPresets, setShowPresets] = useState(false); - const [applyingPreset, setApplyingPreset] = useState(null); - const refresh = async () => { setLoading(true); setError(null); try { - const [ - aliasesData, - rulesData, - exclusionsData, - injectionConfig, - presetsData, - ] = await Promise.all([ - routerApi.getModelAliases(), + const [rulesData, exclusionsData, injectionConfig] = await Promise.all([ routerApi.getRoutingRules(), routerApi.getExclusions(), injectionApi.getInjectionConfig(), - routerApi.getRecommendedPresets(), ]); - setAliases(aliasesData); setRules(rulesData); setExclusions(exclusionsData); setInjectionRules(injectionConfig.rules); setInjectionEnabled(injectionConfig.enabled); - setPresets(presetsData); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { @@ -78,47 +54,6 @@ export const RoutingPage = forwardRef( } }; - const handleApplyPreset = async ( - presetId: string, - merge: boolean = false, - ) => { - setApplyingPreset(presetId); - try { - // 先找到预设配置 - const preset = presets.find((p) => p.id === presetId); - - // 应用别名和规则 - await routerApi.applyRecommendedPreset(presetId, merge); - - // 如果预设包含客户端路由配置,也应用它 - if (preset?.endpoint_providers) { - const ep = preset.endpoint_providers; - const clientTypes = [ - "cursor", - "claude_code", - "codex", - "windsurf", - "kiro", - "other", - ] as const; - for (const clientType of clientTypes) { - const provider = ep[clientType]; - // 只有在非合并模式或有值时才设置 - if (!merge || provider) { - await setEndpointProvider(clientType, provider || null); - } - } - } - - await refresh(); - setShowPresets(false); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - } finally { - setApplyingPreset(null); - } - }; - const handleClearAll = async () => { if (!confirm("确定要清空所有路由配置吗?此操作不可撤销。")) return; try { @@ -137,17 +72,6 @@ export const RoutingPage = forwardRef( refresh(); }, []); - // Alias handlers - const handleAddAlias = async (alias: string, actual: string) => { - await routerApi.addModelAlias(alias, actual); - await refresh(); - }; - - const handleRemoveAlias = async (alias: string) => { - await routerApi.removeModelAlias(alias); - await refresh(); - }; - // Rule handlers const handleAddRule = async (rule: RoutingRule) => { await routerApi.addRoutingRule(rule); @@ -207,7 +131,6 @@ export const RoutingPage = forwardRef( const tabs: { id: TabType; label: string; count: number }[] = [ { id: "clients", label: "客户端路由", count: 0 }, - { id: "aliases", label: "模型别名", count: aliases.length }, { id: "rules", label: "路由规则", count: rules.length }, { id: "exclusions", @@ -226,23 +149,12 @@ export const RoutingPage = forwardRef( 智能路由 -

- 配置模型映射、路由规则和排除列表 -

+

配置路由规则和排除列表

-
)} - {/* Presets Modal */} - setShowPresets(false)} - maxWidth="max-w-2xl" - className="max-h-[80vh] overflow-y-auto" - > -
-
-

- - 推荐配置 -

-
-

- 选择一个预设配置快速设置路由规则和模型别名 -

-
- {presets.map((preset) => ( -
-
-
-

{preset.name}

-

- {preset.description} -

-
- {preset.aliases.length} 个别名 - {preset.rules.length} 条规则 - {preset.endpoint_providers && ( - - { - Object.values(preset.endpoint_providers).filter( - (v) => v, - ).length - }{" "} - 个客户端路由 - - )} -
-
-
- - -
-
-
- ))} -
-
-
-
    -
  • - 模型别名 - :使用熟悉的模型名(如 gpt-4)映射到实际模型 -
  • 路由规则 :将特定模型路由到指定 Provider,支持通配符匹配 @@ -423,14 +247,6 @@ export const RoutingPage = forwardRef(
) : (
- {activeTab === "aliases" && ( - - )} {activeTab === "rules" && ( ; } @@ -60,19 +36,6 @@ export const routerApi = { return invoke("get_router_config"); }, - // Model aliases - async addModelAlias(alias: string, actual: string): Promise { - return invoke("add_model_alias", { alias, actual }); - }, - - async removeModelAlias(alias: string): Promise { - return invoke("remove_model_alias", { alias }); - }, - - async getModelAliases(): Promise { - return invoke("get_model_aliases"); - }, - // Routing rules async addRoutingRule(rule: RoutingRule): Promise { return invoke("add_routing_rule", { rule }); @@ -111,18 +74,6 @@ export const routerApi = { return invoke("set_router_default_provider", { provider }); }, - // Recommended presets - async getRecommendedPresets(): Promise { - return invoke("get_recommended_presets"); - }, - - async applyRecommendedPreset( - presetId: string, - merge: boolean = false, - ): Promise { - return invoke("apply_recommended_preset", { presetId, merge }); - }, - async clearAllRoutingConfig(): Promise { return invoke("clear_all_routing_config"); }, diff --git a/src/lib/plugin-components/global.ts b/src/lib/plugin-components/global.ts index 712d9b0d3..8aacf37cb 100644 --- a/src/lib/plugin-components/global.ts +++ b/src/lib/plugin-components/global.ts @@ -11,6 +11,19 @@ if (typeof window !== "undefined") { (window as unknown as Record).React = React; (window as unknown as Record).ProxyCastPluginComponents = PluginComponents; + + // 调试:检查所有导出 + console.log("[PluginComponents] 已暴露到全局变量"); + console.log("[PluginComponents] 导出的键:", Object.keys(PluginComponents)); + + // 检查是否有 undefined 的导出 + const undefinedExports = Object.entries(PluginComponents) + .filter(([, value]) => value === undefined) + .map(([key]) => key); + + if (undefinedExports.length > 0) { + console.error("[PluginComponents] 以下导出是 undefined:", undefinedExports); + } } export {}; diff --git a/src/lib/plugin-components/index.ts b/src/lib/plugin-components/index.ts index 5a20c6992..a00e44b26 100644 --- a/src/lib/plugin-components/index.ts +++ b/src/lib/plugin-components/index.ts @@ -204,6 +204,8 @@ export { Sparkles, Cookie, FileJson, + Code, + Bot, } from "lucide-react"; // ============================================================================ diff --git a/src/lib/plugin-loader/index.ts b/src/lib/plugin-loader/index.ts index ed09c624f..8df6a7cff 100644 --- a/src/lib/plugin-loader/index.ts +++ b/src/lib/plugin-loader/index.ts @@ -143,6 +143,21 @@ export async function loadPluginUI( `[PluginLoader] 全局变量检查: React=${typeof (window as unknown as Record).React}, ProxyCastPluginComponents=${typeof (window as unknown as Record).ProxyCastPluginComponents}`, ); + // 检查 ProxyCastPluginComponents 中的所有导出 + const components = (window as unknown as Record) + .ProxyCastPluginComponents as Record | undefined; + if (components) { + const undefinedKeys = Object.keys(components).filter( + (key) => components[key] === undefined, + ); + if (undefinedKeys.length > 0) { + console.error( + `[PluginLoader] ProxyCastPluginComponents 中有 undefined 的导出:`, + undefinedKeys, + ); + } + } + // 执行插件代码 await executeScript(content);