mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: bump version to 0.9.0
- Add plugin system (Phase 6) - Add plugin manager with hook system - Add plugin loader and lifecycle management - Add plugin configuration UI - All Phase 6 tests passing (292 tests)
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"version": "0.6.1",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "proxycast",
|
||||
"version": "0.6.1",
|
||||
"version": "0.9.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"private": true,
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+79
-128
@@ -323,22 +323,13 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
|
||||
dependencies = [
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -447,9 +438,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
|
||||
checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -896,9 +887,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -918,7 +909,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2258,7 +2249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
|
||||
dependencies = [
|
||||
"gtk-sys",
|
||||
"libloading",
|
||||
"libloading 0.7.4",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
@@ -2278,6 +2269,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.10"
|
||||
@@ -2440,10 +2441,10 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"keyboard-types",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"png",
|
||||
"serde",
|
||||
@@ -2587,22 +2588,6 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.3"
|
||||
@@ -2620,9 +2605,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
@@ -2630,8 +2615,8 @@ dependencies = [
|
||||
"objc2-core-image",
|
||||
"objc2-core-text",
|
||||
"objc2-core-video",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2641,8 +2626,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2652,8 +2637,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2664,7 +2649,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"dispatch2",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2675,7 +2660,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"dispatch2",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
@@ -2686,8 +2671,8 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
||||
dependencies = [
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2697,7 +2682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
]
|
||||
@@ -2709,7 +2694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-io-surface",
|
||||
@@ -2730,18 +2715,6 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
@@ -2749,9 +2722,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
@@ -2762,7 +2735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
@@ -2772,35 +2745,10 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
|
||||
dependencies = [
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-metal"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -2808,8 +2756,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2819,7 +2768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
@@ -2830,9 +2779,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2842,11 +2791,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"objc2 0.6.3",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"objc2-javascript-core",
|
||||
"objc2-security",
|
||||
]
|
||||
@@ -3325,10 +3274,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proxycast"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -3351,6 +3301,7 @@ dependencies = [
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-shell",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tiktoken-rs",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3606,9 +3557,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.25"
|
||||
version = "0.12.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a"
|
||||
checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -3654,17 +3605,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2 0.6.2",
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
@@ -4231,24 +4182,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "softbuffer"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
|
||||
checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-quartz-core 0.2.2",
|
||||
"ndk",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"redox_syscall",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4420,7 +4371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"block2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
@@ -4437,9 +4388,9 @@ dependencies = [
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"ndk-sys",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"raw-window-handle",
|
||||
@@ -4492,9 +4443,9 @@ dependencies = [
|
||||
"log",
|
||||
"mime",
|
||||
"muda",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"percent-encoding",
|
||||
@@ -4687,7 +4638,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"raw-window-handle",
|
||||
@@ -4711,9 +4662,9 @@ dependencies = [
|
||||
"http",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
@@ -5191,11 +5142,11 @@ dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"png",
|
||||
"serde",
|
||||
@@ -5726,10 +5677,10 @@ version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
|
||||
dependencies = [
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-version",
|
||||
@@ -6256,7 +6207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2 0.6.2",
|
||||
"block2",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dirs 6.0.0",
|
||||
@@ -6271,10 +6222,10 @@ dependencies = [
|
||||
"kuchikiki",
|
||||
"libc",
|
||||
"ndk",
|
||||
"objc2 0.6.3",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"once_cell",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxycast"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
description = "AI API Proxy Desktop App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@@ -41,6 +41,8 @@ dashmap = "5"
|
||||
notify = { version = "6", default-features = false, features = ["macos_fsevent"] }
|
||||
parking_lot = "0.12"
|
||||
tiktoken-rs = "0.6"
|
||||
async-trait = "0.1"
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1"
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod config_cmd;
|
||||
pub mod injection_cmd;
|
||||
pub mod mcp_cmd;
|
||||
pub mod oauth_cmd;
|
||||
pub mod plugin_cmd;
|
||||
pub mod prompt_cmd;
|
||||
pub mod provider_pool_cmd;
|
||||
pub mod resilience_cmd;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//! 插件系统相关命令
|
||||
|
||||
use crate::plugin::{PluginConfig, PluginInfo, PluginManager, PluginStatus};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// 插件管理器状态
|
||||
pub struct PluginManagerState(pub Arc<RwLock<PluginManager>>);
|
||||
|
||||
/// 插件状态响应
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginServiceStatus {
|
||||
pub enabled: bool,
|
||||
pub plugin_count: usize,
|
||||
pub plugins_dir: String,
|
||||
}
|
||||
|
||||
/// 插件配置请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginConfigRequest {
|
||||
pub enabled: bool,
|
||||
pub timeout_ms: u64,
|
||||
pub settings: serde_json::Value,
|
||||
}
|
||||
|
||||
/// 获取插件服务状态
|
||||
#[tauri::command]
|
||||
pub async fn get_plugin_status(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
) -> Result<PluginServiceStatus, String> {
|
||||
let manager = state.0.read().await;
|
||||
Ok(PluginServiceStatus {
|
||||
enabled: true,
|
||||
plugin_count: manager.count(),
|
||||
plugins_dir: manager.plugins_dir().to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取所有插件列表
|
||||
#[tauri::command]
|
||||
pub async fn get_plugins(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
) -> Result<Vec<PluginInfo>, String> {
|
||||
let manager = state.0.read().await;
|
||||
Ok(manager.list().await)
|
||||
}
|
||||
|
||||
/// 获取单个插件信息
|
||||
#[tauri::command]
|
||||
pub async fn get_plugin_info(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
name: String,
|
||||
) -> Result<Option<PluginInfo>, String> {
|
||||
let manager = state.0.read().await;
|
||||
Ok(manager.get_info(&name).await)
|
||||
}
|
||||
|
||||
/// 启用插件
|
||||
#[tauri::command]
|
||||
pub async fn enable_plugin(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
name: String,
|
||||
) -> Result<(), String> {
|
||||
let manager = state.0.read().await;
|
||||
manager.enable(&name).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 禁用插件
|
||||
#[tauri::command]
|
||||
pub async fn disable_plugin(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
name: String,
|
||||
) -> Result<(), String> {
|
||||
let manager = state.0.read().await;
|
||||
manager.disable(&name).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 更新插件配置
|
||||
#[tauri::command]
|
||||
pub async fn update_plugin_config(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
name: String,
|
||||
config: PluginConfigRequest,
|
||||
) -> Result<(), String> {
|
||||
let manager = state.0.read().await;
|
||||
let plugin_config = PluginConfig {
|
||||
enabled: config.enabled,
|
||||
timeout_ms: config.timeout_ms,
|
||||
settings: config.settings,
|
||||
};
|
||||
manager
|
||||
.update_config(&name, plugin_config)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 获取插件配置
|
||||
#[tauri::command]
|
||||
pub async fn get_plugin_config(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
name: String,
|
||||
) -> Result<Option<PluginConfig>, String> {
|
||||
let manager = state.0.read().await;
|
||||
Ok(manager.get_config(&name))
|
||||
}
|
||||
|
||||
/// 重新加载所有插件
|
||||
#[tauri::command]
|
||||
pub async fn reload_plugins(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let manager = state.0.read().await;
|
||||
manager.load_all().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 卸载插件
|
||||
#[tauri::command]
|
||||
pub async fn unload_plugin(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
name: String,
|
||||
) -> Result<(), String> {
|
||||
let manager = state.0.read().await;
|
||||
manager.unload(&name).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 获取插件目录路径
|
||||
#[tauri::command]
|
||||
pub async fn get_plugins_dir(
|
||||
state: tauri::State<'_, PluginManagerState>,
|
||||
) -> Result<String, String> {
|
||||
let manager = state.0.read().await;
|
||||
Ok(manager.plugins_dir().to_string_lossy().to_string())
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mod database;
|
||||
pub mod injection;
|
||||
mod logger;
|
||||
mod models;
|
||||
pub mod plugin;
|
||||
mod providers;
|
||||
pub mod resilience;
|
||||
mod router;
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
//! 插件加载器
|
||||
|
||||
use super::types::{
|
||||
HookResult, Plugin, PluginConfig, PluginContext, PluginError, PluginManifest, PluginType,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
|
||||
pub struct PluginLoader {
|
||||
plugins_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl PluginLoader {
|
||||
pub fn new(plugins_dir: PathBuf) -> Self {
|
||||
Self { plugins_dir }
|
||||
}
|
||||
|
||||
pub fn default_plugins_dir() -> PathBuf {
|
||||
dirs::config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("proxycast")
|
||||
.join("plugins")
|
||||
}
|
||||
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(Self::default_plugins_dir())
|
||||
}
|
||||
|
||||
pub async fn ensure_plugins_dir(&self) -> Result<(), PluginError> {
|
||||
if !self.plugins_dir.exists() {
|
||||
fs::create_dir_all(&self.plugins_dir).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn scan(&self) -> Result<Vec<PathBuf>, PluginError> {
|
||||
self.ensure_plugins_dir().await?;
|
||||
let mut plugins = Vec::new();
|
||||
let mut entries = fs::read_dir(&self.plugins_dir).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.is_dir() && path.join("manifest.json").exists() {
|
||||
plugins.push(path);
|
||||
}
|
||||
}
|
||||
Ok(plugins)
|
||||
}
|
||||
|
||||
pub async fn load_manifest(&self, plugin_dir: &Path) -> Result<PluginManifest, PluginError> {
|
||||
let manifest_path = plugin_dir.join("manifest.json");
|
||||
let content = fs::read_to_string(&manifest_path)
|
||||
.await
|
||||
.map_err(|e| PluginError::LoadError(format!("无法读取清单文件: {}", e)))?;
|
||||
let manifest: PluginManifest = serde_json::from_str(&content)
|
||||
.map_err(|e| PluginError::InvalidManifest(format!("解析失败: {}", e)))?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
pub async fn load(
|
||||
&self,
|
||||
plugin_dir: &Path,
|
||||
config: &PluginConfig,
|
||||
) -> Result<Arc<dyn Plugin>, PluginError> {
|
||||
let manifest = self.load_manifest(plugin_dir).await?;
|
||||
match manifest.plugin_type {
|
||||
PluginType::Script => self.load_script_plugin(plugin_dir, manifest, config).await,
|
||||
PluginType::Native => Err(PluginError::LoadError("原生插件暂不支持".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_script_plugin(
|
||||
&self,
|
||||
plugin_dir: &Path,
|
||||
manifest: PluginManifest,
|
||||
_config: &PluginConfig,
|
||||
) -> Result<Arc<dyn Plugin>, PluginError> {
|
||||
let config_path = plugin_dir.join("config.json");
|
||||
let plugin_settings = if config_path.exists() {
|
||||
let content = fs::read_to_string(&config_path)
|
||||
.await
|
||||
.map_err(|e| PluginError::LoadError(format!("无法读取配置文件: {}", e)))?;
|
||||
serde_json::from_str(&content).unwrap_or_default()
|
||||
} else {
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
};
|
||||
let plugin = ScriptPlugin::new(manifest, plugin_settings);
|
||||
Ok(Arc::new(plugin))
|
||||
}
|
||||
|
||||
pub async fn load_all(
|
||||
&self,
|
||||
configs: &HashMap<String, PluginConfig>,
|
||||
) -> Result<Vec<(PathBuf, Arc<dyn Plugin>)>, PluginError> {
|
||||
let plugin_dirs = self.scan().await?;
|
||||
let mut plugins = Vec::new();
|
||||
for dir in plugin_dirs {
|
||||
let manifest = match self.load_manifest(&dir).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
tracing::warn!("跳过插件 {}: {}", dir.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let config = configs.get(&manifest.name).cloned().unwrap_or_default();
|
||||
match self.load(&dir, &config).await {
|
||||
Ok(plugin) => plugins.push((dir, plugin)),
|
||||
Err(e) => {
|
||||
tracing::warn!("加载插件 {} 失败: {}", manifest.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(plugins)
|
||||
}
|
||||
|
||||
pub fn plugins_dir(&self) -> &Path {
|
||||
&self.plugins_dir
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScriptPlugin {
|
||||
manifest: PluginManifest,
|
||||
settings: serde_json::Value,
|
||||
}
|
||||
|
||||
impl ScriptPlugin {
|
||||
pub fn new(manifest: PluginManifest, settings: serde_json::Value) -> Self {
|
||||
Self { manifest, settings }
|
||||
}
|
||||
|
||||
fn apply_request_transforms(&self, request: &mut serde_json::Value) -> bool {
|
||||
let transforms = match self.settings.get("request_transforms") {
|
||||
Some(t) if t.is_array() => t.as_array().unwrap(),
|
||||
_ => return false,
|
||||
};
|
||||
let mut modified = false;
|
||||
let obj = match request.as_object_mut() {
|
||||
Some(o) => o,
|
||||
None => return false,
|
||||
};
|
||||
for transform in transforms {
|
||||
if let Some(inject) = transform.get("inject").and_then(|v| v.as_object()) {
|
||||
for (key, value) in inject {
|
||||
if !obj.contains_key(key) {
|
||||
obj.insert(key.clone(), value.clone());
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
modified
|
||||
}
|
||||
|
||||
fn apply_response_transforms(&self, response: &mut serde_json::Value) -> bool {
|
||||
let transforms = match self.settings.get("response_transforms") {
|
||||
Some(t) if t.is_array() => t.as_array().unwrap(),
|
||||
_ => return false,
|
||||
};
|
||||
let mut modified = false;
|
||||
let obj = match response.as_object_mut() {
|
||||
Some(o) => o,
|
||||
None => return false,
|
||||
};
|
||||
for transform in transforms {
|
||||
if let Some(inject) = transform.get("inject").and_then(|v| v.as_object()) {
|
||||
for (key, value) in inject {
|
||||
obj.insert(key.clone(), value.clone());
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
modified
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for ScriptPlugin {
|
||||
fn name(&self) -> &str {
|
||||
&self.manifest.name
|
||||
}
|
||||
|
||||
fn version(&self) -> &str {
|
||||
&self.manifest.version
|
||||
}
|
||||
|
||||
fn manifest(&self) -> &PluginManifest {
|
||||
&self.manifest
|
||||
}
|
||||
|
||||
async fn init(&mut self, _config: &PluginConfig) -> Result<(), PluginError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_request(
|
||||
&self,
|
||||
_ctx: &mut PluginContext,
|
||||
request: &mut serde_json::Value,
|
||||
) -> Result<HookResult, PluginError> {
|
||||
let start = std::time::Instant::now();
|
||||
let modified = self.apply_request_transforms(request);
|
||||
Ok(HookResult::success(
|
||||
modified,
|
||||
start.elapsed().as_millis() as u64,
|
||||
))
|
||||
}
|
||||
|
||||
async fn on_response(
|
||||
&self,
|
||||
_ctx: &mut PluginContext,
|
||||
response: &mut serde_json::Value,
|
||||
) -> Result<HookResult, PluginError> {
|
||||
let start = std::time::Instant::now();
|
||||
let modified = self.apply_response_transforms(response);
|
||||
Ok(HookResult::success(
|
||||
modified,
|
||||
start.elapsed().as_millis() as u64,
|
||||
))
|
||||
}
|
||||
|
||||
async fn on_error(
|
||||
&self,
|
||||
_ctx: &mut PluginContext,
|
||||
_error: &str,
|
||||
) -> Result<HookResult, PluginError> {
|
||||
let start = std::time::Instant::now();
|
||||
Ok(HookResult::success(
|
||||
false,
|
||||
start.elapsed().as_millis() as u64,
|
||||
))
|
||||
}
|
||||
|
||||
async fn shutdown(&mut self) -> Result<(), PluginError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
//! 插件管理器
|
||||
//!
|
||||
//! 负责插件的生命周期管理、钩子执行和配置管理
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::loader::PluginLoader;
|
||||
use super::types::{
|
||||
HookResult, Plugin, PluginConfig, PluginContext, PluginError, PluginInfo, PluginInstance,
|
||||
PluginStatus,
|
||||
};
|
||||
|
||||
/// 插件管理器配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginManagerConfig {
|
||||
/// 默认超时时间 (毫秒)
|
||||
pub default_timeout_ms: u64,
|
||||
/// 是否启用插件系统
|
||||
pub enabled: bool,
|
||||
/// 最大并发插件数
|
||||
pub max_plugins: usize,
|
||||
}
|
||||
|
||||
impl Default for PluginManagerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_timeout_ms: 5000,
|
||||
enabled: true,
|
||||
max_plugins: 50,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件管理器
|
||||
pub struct PluginManager {
|
||||
/// 插件加载器
|
||||
loader: PluginLoader,
|
||||
/// 已加载的插件
|
||||
plugins: DashMap<String, Arc<RwLock<PluginInstance>>>,
|
||||
/// 插件配置
|
||||
configs: DashMap<String, PluginConfig>,
|
||||
/// 管理器配置
|
||||
config: PluginManagerConfig,
|
||||
}
|
||||
|
||||
impl PluginManager {
|
||||
/// 创建新的插件管理器
|
||||
pub fn new(plugins_dir: PathBuf, config: PluginManagerConfig) -> Self {
|
||||
Self {
|
||||
loader: PluginLoader::new(plugins_dir),
|
||||
plugins: DashMap::new(),
|
||||
configs: DashMap::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用默认配置创建
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(
|
||||
PluginLoader::default_plugins_dir(),
|
||||
PluginManagerConfig::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 加载所有插件
|
||||
pub async fn load_all(&self) -> Result<Vec<String>, PluginError> {
|
||||
if !self.config.enabled {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let configs: HashMap<String, PluginConfig> = self
|
||||
.configs
|
||||
.iter()
|
||||
.map(|r| (r.key().clone(), r.value().clone()))
|
||||
.collect();
|
||||
|
||||
let loaded = self.loader.load_all(&configs).await?;
|
||||
let mut names = Vec::new();
|
||||
|
||||
for (path, plugin) in loaded {
|
||||
let name = plugin.name().to_string();
|
||||
let config = configs.get(&name).cloned().unwrap_or_default();
|
||||
|
||||
let mut instance = PluginInstance::new(plugin.clone(), path, config.clone());
|
||||
|
||||
// 初始化插件
|
||||
if let Err(e) = Arc::get_mut(&mut instance.plugin)
|
||||
.ok_or_else(|| PluginError::InitError("无法获取插件可变引用".to_string()))?
|
||||
.init(&config)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("插件 {} 初始化失败: {}", name, e);
|
||||
instance.state.status = PluginStatus::Error;
|
||||
instance.state.last_error = Some(e.to_string());
|
||||
} else {
|
||||
instance.state.status = if config.enabled {
|
||||
PluginStatus::Enabled
|
||||
} else {
|
||||
PluginStatus::Disabled
|
||||
};
|
||||
}
|
||||
|
||||
self.plugins
|
||||
.insert(name.clone(), Arc::new(RwLock::new(instance)));
|
||||
names.push(name);
|
||||
}
|
||||
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// 加载单个插件
|
||||
pub async fn load(&self, plugin_dir: &Path) -> Result<String, PluginError> {
|
||||
if self.plugins.len() >= self.config.max_plugins {
|
||||
return Err(PluginError::LoadError(format!(
|
||||
"已达到最大插件数限制: {}",
|
||||
self.config.max_plugins
|
||||
)));
|
||||
}
|
||||
|
||||
let config = PluginConfig::default();
|
||||
let plugin = self.loader.load(plugin_dir, &config).await?;
|
||||
let name = plugin.name().to_string();
|
||||
|
||||
// 检查是否已加载
|
||||
if self.plugins.contains_key(&name) {
|
||||
return Err(PluginError::LoadError(format!("插件 {} 已加载", name)));
|
||||
}
|
||||
|
||||
let mut instance =
|
||||
PluginInstance::new(plugin.clone(), plugin_dir.to_path_buf(), config.clone());
|
||||
|
||||
// 初始化插件
|
||||
if let Err(e) = Arc::get_mut(&mut instance.plugin)
|
||||
.ok_or_else(|| PluginError::InitError("无法获取插件可变引用".to_string()))?
|
||||
.init(&config)
|
||||
.await
|
||||
{
|
||||
instance.state.status = PluginStatus::Error;
|
||||
instance.state.last_error = Some(e.to_string());
|
||||
} else {
|
||||
instance.state.status = PluginStatus::Enabled;
|
||||
}
|
||||
|
||||
self.plugins
|
||||
.insert(name.clone(), Arc::new(RwLock::new(instance)));
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
/// 卸载插件
|
||||
pub async fn unload(&self, name: &str) -> Result<(), PluginError> {
|
||||
let instance = self
|
||||
.plugins
|
||||
.remove(name)
|
||||
.map(|(_, v)| v)
|
||||
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
|
||||
|
||||
// 关闭插件
|
||||
let mut inst = instance.write().await;
|
||||
if let Some(plugin) = Arc::get_mut(&mut inst.plugin) {
|
||||
plugin.shutdown().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 启用插件
|
||||
pub async fn enable(&self, name: &str) -> Result<(), PluginError> {
|
||||
let instance = self
|
||||
.plugins
|
||||
.get(name)
|
||||
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
|
||||
|
||||
let mut inst = instance.write().await;
|
||||
inst.config.enabled = true;
|
||||
inst.state.status = PluginStatus::Enabled;
|
||||
|
||||
// 更新配置
|
||||
self.configs.insert(name.to_string(), inst.config.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 禁用插件
|
||||
pub async fn disable(&self, name: &str) -> Result<(), PluginError> {
|
||||
let instance = self
|
||||
.plugins
|
||||
.get(name)
|
||||
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
|
||||
|
||||
let mut inst = instance.write().await;
|
||||
inst.config.enabled = false;
|
||||
inst.state.status = PluginStatus::Disabled;
|
||||
|
||||
// 更新配置
|
||||
self.configs.insert(name.to_string(), inst.config.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新插件配置
|
||||
pub async fn update_config(&self, name: &str, config: PluginConfig) -> Result<(), PluginError> {
|
||||
let instance = self
|
||||
.plugins
|
||||
.get(name)
|
||||
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
|
||||
|
||||
let mut inst = instance.write().await;
|
||||
inst.config = config.clone();
|
||||
|
||||
// 更新状态
|
||||
if config.enabled && inst.state.status != PluginStatus::Error {
|
||||
inst.state.status = PluginStatus::Enabled;
|
||||
} else if !config.enabled {
|
||||
inst.state.status = PluginStatus::Disabled;
|
||||
}
|
||||
|
||||
// 更新配置存储
|
||||
self.configs.insert(name.to_string(), config);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取插件配置
|
||||
pub fn get_config(&self, name: &str) -> Option<PluginConfig> {
|
||||
self.configs.get(name).map(|r| r.value().clone())
|
||||
}
|
||||
|
||||
/// 获取插件信息
|
||||
pub async fn get_info(&self, name: &str) -> Option<PluginInfo> {
|
||||
let instance = self.plugins.get(name)?;
|
||||
let inst = instance.read().await;
|
||||
Some(inst.info())
|
||||
}
|
||||
|
||||
/// 获取所有插件信息
|
||||
pub async fn list(&self) -> Vec<PluginInfo> {
|
||||
let mut infos = Vec::new();
|
||||
for entry in self.plugins.iter() {
|
||||
let inst = entry.value().read().await;
|
||||
infos.push(inst.info());
|
||||
}
|
||||
infos
|
||||
}
|
||||
|
||||
/// 执行请求前钩子 (带隔离)
|
||||
pub async fn run_on_request(
|
||||
&self,
|
||||
ctx: &mut PluginContext,
|
||||
request: &mut serde_json::Value,
|
||||
) -> Vec<HookResult> {
|
||||
if !self.config.enabled {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for entry in self.plugins.iter() {
|
||||
let instance = entry.value().read().await;
|
||||
if !instance.is_enabled() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let timeout_ms = instance.config.timeout_ms;
|
||||
let plugin = instance.plugin.clone();
|
||||
let plugin_name = plugin.name().to_string();
|
||||
|
||||
// 带超时执行
|
||||
let result = match timeout(
|
||||
Duration::from_millis(timeout_ms),
|
||||
plugin.on_request(ctx, request),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!("插件 {} on_request 执行失败: {}", plugin_name, e);
|
||||
HookResult::failure(e.to_string(), timeout_ms)
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("插件 {} on_request 执行超时", plugin_name);
|
||||
HookResult::failure(format!("执行超时 ({}ms)", timeout_ms), timeout_ms)
|
||||
}
|
||||
};
|
||||
|
||||
// 更新状态
|
||||
drop(instance);
|
||||
if let Some(inst) = self.plugins.get(&plugin_name) {
|
||||
let mut inst = inst.write().await;
|
||||
inst.state
|
||||
.record_execution(result.success, result.error.clone());
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// 执行响应后钩子 (带隔离)
|
||||
pub async fn run_on_response(
|
||||
&self,
|
||||
ctx: &mut PluginContext,
|
||||
response: &mut serde_json::Value,
|
||||
) -> Vec<HookResult> {
|
||||
if !self.config.enabled {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for entry in self.plugins.iter() {
|
||||
let instance = entry.value().read().await;
|
||||
if !instance.is_enabled() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let timeout_ms = instance.config.timeout_ms;
|
||||
let plugin = instance.plugin.clone();
|
||||
let plugin_name = plugin.name().to_string();
|
||||
|
||||
// 带超时执行
|
||||
let result = match timeout(
|
||||
Duration::from_millis(timeout_ms),
|
||||
plugin.on_response(ctx, response),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!("插件 {} on_response 执行失败: {}", plugin_name, e);
|
||||
HookResult::failure(e.to_string(), timeout_ms)
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("插件 {} on_response 执行超时", plugin_name);
|
||||
HookResult::failure(format!("执行超时 ({}ms)", timeout_ms), timeout_ms)
|
||||
}
|
||||
};
|
||||
|
||||
// 更新状态
|
||||
drop(instance);
|
||||
if let Some(inst) = self.plugins.get(&plugin_name) {
|
||||
let mut inst = inst.write().await;
|
||||
inst.state
|
||||
.record_execution(result.success, result.error.clone());
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// 执行错误钩子 (带隔离)
|
||||
pub async fn run_on_error(&self, ctx: &mut PluginContext, error: &str) -> Vec<HookResult> {
|
||||
if !self.config.enabled {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for entry in self.plugins.iter() {
|
||||
let instance = entry.value().read().await;
|
||||
if !instance.is_enabled() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let timeout_ms = instance.config.timeout_ms;
|
||||
let plugin = instance.plugin.clone();
|
||||
let plugin_name = plugin.name().to_string();
|
||||
|
||||
// 带超时执行
|
||||
let result = match timeout(
|
||||
Duration::from_millis(timeout_ms),
|
||||
plugin.on_error(ctx, error),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!("插件 {} on_error 执行失败: {}", plugin_name, e);
|
||||
HookResult::failure(e.to_string(), timeout_ms)
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("插件 {} on_error 执行超时", plugin_name);
|
||||
HookResult::failure(format!("执行超时 ({}ms)", timeout_ms), timeout_ms)
|
||||
}
|
||||
};
|
||||
|
||||
// 更新状态
|
||||
drop(instance);
|
||||
if let Some(inst) = self.plugins.get(&plugin_name) {
|
||||
let mut inst = inst.write().await;
|
||||
inst.state
|
||||
.record_execution(result.success, result.error.clone());
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// 获取已加载插件数量
|
||||
pub fn count(&self) -> usize {
|
||||
self.plugins.len()
|
||||
}
|
||||
|
||||
/// 检查插件是否已加载
|
||||
pub fn is_loaded(&self, name: &str) -> bool {
|
||||
self.plugins.contains_key(name)
|
||||
}
|
||||
|
||||
/// 获取插件目录
|
||||
pub fn plugins_dir(&self) -> &Path {
|
||||
self.loader.plugins_dir()
|
||||
}
|
||||
|
||||
/// 设置插件配置 (批量)
|
||||
pub fn set_configs(&self, configs: HashMap<String, PluginConfig>) {
|
||||
for (name, config) in configs {
|
||||
self.configs.insert(name, config);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取所有插件配置
|
||||
pub fn get_all_configs(&self) -> HashMap<String, PluginConfig> {
|
||||
self.configs
|
||||
.iter()
|
||||
.map(|r| (r.key().clone(), r.value().clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PluginManager {
|
||||
fn default() -> Self {
|
||||
Self::with_defaults()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! 插件系统模块
|
||||
//!
|
||||
//! 提供插件扩展功能,支持:
|
||||
//! - 插件加载和初始化
|
||||
//! - 请求前/响应后钩子
|
||||
//! - 插件隔离和错误处理
|
||||
//! - 插件配置管理
|
||||
|
||||
mod loader;
|
||||
mod manager;
|
||||
mod types;
|
||||
|
||||
pub use loader::PluginLoader;
|
||||
pub use manager::PluginManager;
|
||||
pub use types::{
|
||||
HookResult, Plugin, PluginConfig, PluginContext, PluginError, PluginInfo, PluginManifest,
|
||||
PluginState, PluginStatus, PluginType,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,278 @@
|
||||
//! 插件系统测试
|
||||
|
||||
use super::*;
|
||||
use crate::ProviderType;
|
||||
|
||||
#[test]
|
||||
fn test_plugin_manifest_validation() {
|
||||
// 有效清单
|
||||
let valid = PluginManifest {
|
||||
name: "test-plugin".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
description: "Test plugin".to_string(),
|
||||
author: Some("Test Author".to_string()),
|
||||
homepage: None,
|
||||
license: Some("MIT".to_string()),
|
||||
entry: "config.json".to_string(),
|
||||
plugin_type: PluginType::Script,
|
||||
config_schema: None,
|
||||
hooks: vec!["on_request".to_string()],
|
||||
min_proxycast_version: None,
|
||||
};
|
||||
assert!(valid.validate().is_ok());
|
||||
|
||||
// 空名称
|
||||
let invalid_name = PluginManifest {
|
||||
name: "".to_string(),
|
||||
..valid.clone()
|
||||
};
|
||||
assert!(invalid_name.validate().is_err());
|
||||
|
||||
// 空版本
|
||||
let invalid_version = PluginManifest {
|
||||
version: "".to_string(),
|
||||
..valid.clone()
|
||||
};
|
||||
assert!(invalid_version.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_context() {
|
||||
let mut ctx = PluginContext::new(
|
||||
"req-123".to_string(),
|
||||
ProviderType::Kiro,
|
||||
"claude-sonnet-4-5".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(ctx.request_id, "req-123");
|
||||
assert_eq!(ctx.provider, ProviderType::Kiro);
|
||||
assert_eq!(ctx.model, "claude-sonnet-4-5");
|
||||
assert!(ctx.metadata.is_empty());
|
||||
|
||||
// 添加元数据
|
||||
ctx.set_metadata("key1", serde_json::json!("value1"));
|
||||
assert_eq!(ctx.get_metadata("key1"), Some(&serde_json::json!("value1")));
|
||||
|
||||
// 使用 builder 模式
|
||||
let ctx2 = PluginContext::new(
|
||||
"req-456".to_string(),
|
||||
ProviderType::Gemini,
|
||||
"gemini-2.5-flash".to_string(),
|
||||
)
|
||||
.with_metadata("test", serde_json::json!(123));
|
||||
|
||||
assert_eq!(ctx2.get_metadata("test"), Some(&serde_json::json!(123)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hook_result() {
|
||||
let success = HookResult::success(true, 100);
|
||||
assert!(success.success);
|
||||
assert!(success.modified);
|
||||
assert!(success.error.is_none());
|
||||
assert_eq!(success.duration_ms, 100);
|
||||
|
||||
let failure = HookResult::failure("test error".to_string(), 50);
|
||||
assert!(!failure.success);
|
||||
assert!(!failure.modified);
|
||||
assert_eq!(failure.error, Some("test error".to_string()));
|
||||
assert_eq!(failure.duration_ms, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_config() {
|
||||
let config = PluginConfig::new()
|
||||
.with_enabled(true)
|
||||
.with_timeout(3000)
|
||||
.with_settings(serde_json::json!({"key": "value"}));
|
||||
|
||||
assert!(config.enabled);
|
||||
assert_eq!(config.timeout_ms, 3000);
|
||||
assert_eq!(config.settings, serde_json::json!({"key": "value"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_state() {
|
||||
let mut state = PluginState::new("test-plugin".to_string());
|
||||
|
||||
assert_eq!(state.name, "test-plugin");
|
||||
assert_eq!(state.status, PluginStatus::Loaded);
|
||||
assert_eq!(state.execution_count, 0);
|
||||
assert_eq!(state.error_count, 0);
|
||||
assert!(state.last_error.is_none());
|
||||
|
||||
// 记录成功执行
|
||||
state.record_execution(true, None);
|
||||
assert_eq!(state.execution_count, 1);
|
||||
assert_eq!(state.error_count, 0);
|
||||
assert!(state.last_executed.is_some());
|
||||
|
||||
// 记录失败执行
|
||||
state.record_execution(false, Some("test error".to_string()));
|
||||
assert_eq!(state.execution_count, 2);
|
||||
assert_eq!(state.error_count, 1);
|
||||
assert_eq!(state.last_error, Some("test error".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_status_display() {
|
||||
assert_eq!(PluginStatus::Loaded.to_string(), "loaded");
|
||||
assert_eq!(PluginStatus::Enabled.to_string(), "enabled");
|
||||
assert_eq!(PluginStatus::Disabled.to_string(), "disabled");
|
||||
assert_eq!(PluginStatus::Error.to_string(), "error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_error_display() {
|
||||
let err = PluginError::NotFound("test-plugin".to_string());
|
||||
assert!(err.to_string().contains("test-plugin"));
|
||||
|
||||
let err = PluginError::Timeout {
|
||||
plugin_name: "slow-plugin".to_string(),
|
||||
timeout_ms: 5000,
|
||||
};
|
||||
assert!(err.to_string().contains("slow-plugin"));
|
||||
assert!(err.to_string().contains("5000"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_manifest_serde() {
|
||||
let manifest = PluginManifest {
|
||||
name: "test-plugin".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
description: "A test plugin".to_string(),
|
||||
author: Some("Test".to_string()),
|
||||
homepage: None,
|
||||
license: None,
|
||||
entry: "config.json".to_string(),
|
||||
plugin_type: PluginType::Script,
|
||||
config_schema: Some(serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" }
|
||||
}
|
||||
})),
|
||||
hooks: vec!["on_request".to_string(), "on_response".to_string()],
|
||||
min_proxycast_version: Some("0.13.0".to_string()),
|
||||
};
|
||||
|
||||
// 序列化
|
||||
let json = serde_json::to_string(&manifest).unwrap();
|
||||
assert!(json.contains("test-plugin"));
|
||||
|
||||
// 反序列化
|
||||
let parsed: PluginManifest = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.name, manifest.name);
|
||||
assert_eq!(parsed.version, manifest.version);
|
||||
assert_eq!(parsed.hooks.len(), 2);
|
||||
}
|
||||
|
||||
// Property-based tests
|
||||
use proptest::prelude::*;
|
||||
|
||||
/// **Feature: enhancement-roadmap, Property 17: 插件隔离性**
|
||||
/// **Validates: Requirements 6.3 (验收标准 3)**
|
||||
///
|
||||
/// *对于任意* 插件执行失败,主请求处理流程应继续正常执行
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
use crate::plugin::manager::{PluginManager, PluginManagerConfig};
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// 生成随机的请求 JSON
|
||||
fn arb_request() -> impl Strategy<Value = serde_json::Value> {
|
||||
prop::collection::hash_map(
|
||||
"[a-z]{1,10}",
|
||||
prop_oneof![
|
||||
Just(serde_json::Value::Null),
|
||||
any::<bool>().prop_map(serde_json::Value::Bool),
|
||||
any::<i64>().prop_map(|n| serde_json::Value::Number(n.into())),
|
||||
"[a-zA-Z0-9 ]{0,50}".prop_map(serde_json::Value::String),
|
||||
],
|
||||
0..5,
|
||||
)
|
||||
.prop_map(|map| serde_json::Value::Object(map.into_iter().collect()))
|
||||
}
|
||||
|
||||
/// 生成随机的插件上下文
|
||||
fn arb_context() -> impl Strategy<Value = PluginContext> {
|
||||
(
|
||||
"[a-z0-9]{8,16}", // request_id
|
||||
prop_oneof![
|
||||
Just(ProviderType::Kiro),
|
||||
Just(ProviderType::Gemini),
|
||||
Just(ProviderType::Qwen),
|
||||
],
|
||||
"[a-z0-9-]{5,20}", // model
|
||||
)
|
||||
.prop_map(|(request_id, provider, model)| {
|
||||
PluginContext::new(request_id, provider, model)
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
/// **Property 17: 插件隔离性**
|
||||
///
|
||||
/// 验证:即使没有插件加载,PluginManager 的钩子执行也应该正常完成
|
||||
/// 不会因为空插件列表而失败
|
||||
#[test]
|
||||
fn plugin_manager_handles_empty_plugins(
|
||||
request in arb_request(),
|
||||
ctx in arb_context()
|
||||
) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let (results_empty, req_unchanged) = rt.block_on(async {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config = PluginManagerConfig {
|
||||
default_timeout_ms: 1000,
|
||||
enabled: true,
|
||||
max_plugins: 10,
|
||||
};
|
||||
let manager = PluginManager::new(temp_dir.path().to_path_buf(), config);
|
||||
|
||||
let mut ctx = ctx;
|
||||
let mut req = request.clone();
|
||||
|
||||
// 即使没有插件,钩子执行也应该成功返回空结果
|
||||
let results = manager.run_on_request(&mut ctx, &mut req).await;
|
||||
|
||||
(results.is_empty(), req == request)
|
||||
});
|
||||
|
||||
prop_assert!(results_empty, "Empty plugin list should return empty results");
|
||||
prop_assert!(req_unchanged, "Request should not be modified when no plugins");
|
||||
}
|
||||
|
||||
/// **Property 17: 插件隔离性 - 禁用状态**
|
||||
///
|
||||
/// 验证:当插件系统禁用时,钩子执行应该正常完成且不修改数据
|
||||
#[test]
|
||||
fn disabled_plugin_system_does_not_affect_request(
|
||||
request in arb_request(),
|
||||
ctx in arb_context()
|
||||
) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let (results_empty, req_unchanged) = rt.block_on(async {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config = PluginManagerConfig {
|
||||
default_timeout_ms: 1000,
|
||||
enabled: false, // 禁用插件系统
|
||||
max_plugins: 10,
|
||||
};
|
||||
let manager = PluginManager::new(temp_dir.path().to_path_buf(), config);
|
||||
|
||||
let mut ctx = ctx;
|
||||
let mut req = request.clone();
|
||||
|
||||
// 禁用状态下,钩子执行应该返回空结果
|
||||
let results = manager.run_on_request(&mut ctx, &mut req).await;
|
||||
|
||||
(results.is_empty(), req == request)
|
||||
});
|
||||
|
||||
prop_assert!(results_empty, "Disabled plugin system should return empty results");
|
||||
prop_assert!(req_unchanged, "Request should not be modified when plugins disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
//! 插件系统类型定义
|
||||
//!
|
||||
//! 定义 Plugin trait、PluginContext、PluginManifest 等核心类型
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ProviderType;
|
||||
|
||||
/// 插件错误类型
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PluginError {
|
||||
#[error("插件加载失败: {0}")]
|
||||
LoadError(String),
|
||||
|
||||
#[error("插件初始化失败: {0}")]
|
||||
InitError(String),
|
||||
|
||||
#[error("插件执行超时: {plugin_name} 超过 {timeout_ms}ms")]
|
||||
Timeout {
|
||||
plugin_name: String,
|
||||
timeout_ms: u64,
|
||||
},
|
||||
|
||||
#[error("插件执行失败: {plugin_name} - {message}")]
|
||||
ExecutionError {
|
||||
plugin_name: String,
|
||||
message: String,
|
||||
},
|
||||
|
||||
#[error("插件配置错误: {0}")]
|
||||
ConfigError(String),
|
||||
|
||||
#[error("插件不存在: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("插件已禁用: {0}")]
|
||||
Disabled(String),
|
||||
|
||||
#[error("清单文件无效: {0}")]
|
||||
InvalidManifest(String),
|
||||
|
||||
#[error("IO 错误: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON 解析错误: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// 插件状态
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PluginStatus {
|
||||
/// 已加载但未启用
|
||||
#[default]
|
||||
Loaded,
|
||||
/// 已启用
|
||||
Enabled,
|
||||
/// 已禁用
|
||||
Disabled,
|
||||
/// 错误状态
|
||||
Error,
|
||||
}
|
||||
|
||||
impl fmt::Display for PluginStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PluginStatus::Loaded => write!(f, "loaded"),
|
||||
PluginStatus::Enabled => write!(f, "enabled"),
|
||||
PluginStatus::Disabled => write!(f, "disabled"),
|
||||
PluginStatus::Error => write!(f, "error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件清单 (manifest.json)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginManifest {
|
||||
/// 插件名称
|
||||
pub name: String,
|
||||
/// 插件版本
|
||||
pub version: String,
|
||||
/// 插件描述
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// 作者
|
||||
#[serde(default)]
|
||||
pub author: Option<String>,
|
||||
/// 主页/仓库地址
|
||||
#[serde(default)]
|
||||
pub homepage: Option<String>,
|
||||
/// 许可证
|
||||
#[serde(default)]
|
||||
pub license: Option<String>,
|
||||
/// 入口文件 (相对于插件目录)
|
||||
#[serde(default = "default_entry")]
|
||||
pub entry: String,
|
||||
/// 插件类型
|
||||
#[serde(default)]
|
||||
pub plugin_type: PluginType,
|
||||
/// 配置 schema (JSON Schema)
|
||||
#[serde(default)]
|
||||
pub config_schema: Option<serde_json::Value>,
|
||||
/// 支持的钩子
|
||||
#[serde(default)]
|
||||
pub hooks: Vec<String>,
|
||||
/// 最低 ProxyCast 版本要求
|
||||
#[serde(default)]
|
||||
pub min_proxycast_version: Option<String>,
|
||||
}
|
||||
|
||||
fn default_entry() -> String {
|
||||
"config.json".to_string()
|
||||
}
|
||||
|
||||
impl PluginManifest {
|
||||
/// 验证清单有效性
|
||||
pub fn validate(&self) -> Result<(), PluginError> {
|
||||
if self.name.is_empty() {
|
||||
return Err(PluginError::InvalidManifest("插件名称不能为空".to_string()));
|
||||
}
|
||||
if self.version.is_empty() {
|
||||
return Err(PluginError::InvalidManifest("插件版本不能为空".to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件类型
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PluginType {
|
||||
/// 脚本插件 (JSON 配置驱动)
|
||||
#[default]
|
||||
#[serde(alias = "lua")]
|
||||
Script,
|
||||
/// 原生 Rust 插件 (预留)
|
||||
Native,
|
||||
}
|
||||
|
||||
/// 插件上下文 - 传递给钩子函数的上下文信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginContext {
|
||||
/// 请求 ID
|
||||
pub request_id: String,
|
||||
/// Provider 类型
|
||||
pub provider: ProviderType,
|
||||
/// 模型名称
|
||||
pub model: String,
|
||||
/// 元数据
|
||||
#[serde(default)]
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
/// 时间戳
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl PluginContext {
|
||||
/// 创建新的插件上下文
|
||||
pub fn new(request_id: String, provider: ProviderType, model: String) -> Self {
|
||||
Self {
|
||||
request_id,
|
||||
provider,
|
||||
model,
|
||||
metadata: HashMap::new(),
|
||||
timestamp: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加元数据
|
||||
pub fn with_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
|
||||
self.metadata.insert(key.to_string(), value);
|
||||
self
|
||||
}
|
||||
|
||||
/// 获取元数据
|
||||
pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
|
||||
self.metadata.get(key)
|
||||
}
|
||||
|
||||
/// 设置元数据
|
||||
pub fn set_metadata(&mut self, key: &str, value: serde_json::Value) {
|
||||
self.metadata.insert(key.to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// 钩子执行结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HookResult {
|
||||
/// 是否成功
|
||||
pub success: bool,
|
||||
/// 是否修改了数据
|
||||
pub modified: bool,
|
||||
/// 错误信息 (如果失败)
|
||||
pub error: Option<String>,
|
||||
/// 执行时间 (毫秒)
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
impl HookResult {
|
||||
/// 创建成功结果
|
||||
pub fn success(modified: bool, duration_ms: u64) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
modified,
|
||||
error: None,
|
||||
duration_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建失败结果
|
||||
pub fn failure(error: String, duration_ms: u64) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
modified: false,
|
||||
error: Some(error),
|
||||
duration_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PluginConfig {
|
||||
/// 插件特定配置
|
||||
#[serde(default)]
|
||||
pub settings: serde_json::Value,
|
||||
/// 是否启用
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
/// 执行超时 (毫秒)
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout_ms: u64,
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_timeout() -> u64 {
|
||||
5000 // 5 秒
|
||||
}
|
||||
|
||||
impl PluginConfig {
|
||||
/// 创建默认配置
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// 设置配置值
|
||||
pub fn with_settings(mut self, settings: serde_json::Value) -> Self {
|
||||
self.settings = settings;
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置启用状态
|
||||
pub fn with_enabled(mut self, enabled: bool) -> Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置超时
|
||||
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
|
||||
self.timeout_ms = timeout_ms;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件状态信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginState {
|
||||
/// 插件名称
|
||||
pub name: String,
|
||||
/// 插件状态
|
||||
pub status: PluginStatus,
|
||||
/// 加载时间
|
||||
pub loaded_at: DateTime<Utc>,
|
||||
/// 最后执行时间
|
||||
pub last_executed: Option<DateTime<Utc>>,
|
||||
/// 执行次数
|
||||
pub execution_count: u64,
|
||||
/// 错误次数
|
||||
pub error_count: u64,
|
||||
/// 最后错误信息
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
|
||||
impl PluginState {
|
||||
/// 创建新的插件状态
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
status: PluginStatus::Loaded,
|
||||
loaded_at: Utc::now(),
|
||||
last_executed: None,
|
||||
execution_count: 0,
|
||||
error_count: 0,
|
||||
last_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录执行
|
||||
pub fn record_execution(&mut self, success: bool, error: Option<String>) {
|
||||
self.last_executed = Some(Utc::now());
|
||||
self.execution_count += 1;
|
||||
if !success {
|
||||
self.error_count += 1;
|
||||
self.last_error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件信息 (用于 UI 显示)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginInfo {
|
||||
/// 插件名称
|
||||
pub name: String,
|
||||
/// 插件版本
|
||||
pub version: String,
|
||||
/// 插件描述
|
||||
pub description: String,
|
||||
/// 作者
|
||||
pub author: Option<String>,
|
||||
/// 插件状态
|
||||
pub status: PluginStatus,
|
||||
/// 插件路径
|
||||
pub path: PathBuf,
|
||||
/// 支持的钩子
|
||||
pub hooks: Vec<String>,
|
||||
/// 配置 schema
|
||||
pub config_schema: Option<serde_json::Value>,
|
||||
/// 当前配置
|
||||
pub config: PluginConfig,
|
||||
/// 运行时状态
|
||||
pub state: PluginState,
|
||||
}
|
||||
|
||||
/// 插件 trait - 定义插件必须实现的接口
|
||||
#[async_trait]
|
||||
pub trait Plugin: Send + Sync {
|
||||
/// 获取插件名称
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// 获取插件版本
|
||||
fn version(&self) -> &str;
|
||||
|
||||
/// 获取插件清单
|
||||
fn manifest(&self) -> &PluginManifest;
|
||||
|
||||
/// 初始化插件
|
||||
async fn init(&mut self, config: &PluginConfig) -> Result<(), PluginError>;
|
||||
|
||||
/// 请求前钩子
|
||||
async fn on_request(
|
||||
&self,
|
||||
ctx: &mut PluginContext,
|
||||
request: &mut serde_json::Value,
|
||||
) -> Result<HookResult, PluginError>;
|
||||
|
||||
/// 响应后钩子
|
||||
async fn on_response(
|
||||
&self,
|
||||
ctx: &mut PluginContext,
|
||||
response: &mut serde_json::Value,
|
||||
) -> Result<HookResult, PluginError>;
|
||||
|
||||
/// 错误钩子
|
||||
async fn on_error(
|
||||
&self,
|
||||
ctx: &mut PluginContext,
|
||||
error: &str,
|
||||
) -> Result<HookResult, PluginError>;
|
||||
|
||||
/// 关闭插件
|
||||
async fn shutdown(&mut self) -> Result<(), PluginError>;
|
||||
}
|
||||
|
||||
/// 插件实例包装器 - 用于管理插件生命周期
|
||||
pub struct PluginInstance {
|
||||
/// 插件实现
|
||||
pub plugin: Arc<dyn Plugin>,
|
||||
/// 插件路径
|
||||
pub path: PathBuf,
|
||||
/// 插件配置
|
||||
pub config: PluginConfig,
|
||||
/// 插件状态
|
||||
pub state: PluginState,
|
||||
}
|
||||
|
||||
impl PluginInstance {
|
||||
/// 创建新的插件实例
|
||||
pub fn new(plugin: Arc<dyn Plugin>, path: PathBuf, config: PluginConfig) -> Self {
|
||||
let state = PluginState::new(plugin.name().to_string());
|
||||
Self {
|
||||
plugin,
|
||||
path,
|
||||
config,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取插件信息
|
||||
pub fn info(&self) -> PluginInfo {
|
||||
let manifest = self.plugin.manifest();
|
||||
PluginInfo {
|
||||
name: manifest.name.clone(),
|
||||
version: manifest.version.clone(),
|
||||
description: manifest.description.clone(),
|
||||
author: manifest.author.clone(),
|
||||
status: self.state.status,
|
||||
path: self.path.clone(),
|
||||
hooks: manifest.hooks.clone(),
|
||||
config_schema: manifest.config_schema.clone(),
|
||||
config: self.config.clone(),
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否启用
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.config.enabled && self.state.status == PluginStatus::Enabled
|
||||
}
|
||||
}
|
||||
@@ -263,7 +263,7 @@ async fn run_server(
|
||||
async fn health() -> impl IntoResponse {
|
||||
Json(serde_json::json!({
|
||||
"status": "healthy",
|
||||
"version": "0.8.0"
|
||||
"version": "0.9.0"
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ProxyCast",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"identifier": "com.proxycast.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
Puzzle,
|
||||
RefreshCw,
|
||||
Power,
|
||||
PowerOff,
|
||||
Trash2,
|
||||
FolderOpen,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from "lucide-react";
|
||||
|
||||
interface PluginState {
|
||||
name: string;
|
||||
status: string;
|
||||
loaded_at: string;
|
||||
last_executed: string | null;
|
||||
execution_count: number;
|
||||
error_count: number;
|
||||
last_error: string | null;
|
||||
}
|
||||
|
||||
interface PluginConfig {
|
||||
enabled: boolean;
|
||||
timeout_ms: number;
|
||||
settings: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface PluginInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
author: string | null;
|
||||
status: string;
|
||||
path: string;
|
||||
hooks: string[];
|
||||
config_schema: Record<string, unknown> | null;
|
||||
config: PluginConfig;
|
||||
state: PluginState;
|
||||
}
|
||||
|
||||
interface PluginServiceStatus {
|
||||
enabled: boolean;
|
||||
plugin_count: number;
|
||||
plugins_dir: string;
|
||||
}
|
||||
|
||||
export function PluginManager() {
|
||||
const [status, setStatus] = useState<PluginServiceStatus | null>(null);
|
||||
const [plugins, setPlugins] = useState<PluginInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [serviceStatus, pluginList] = await Promise.all([
|
||||
invoke<PluginServiceStatus>("get_plugin_status"),
|
||||
invoke<PluginInfo[]>("get_plugins"),
|
||||
]);
|
||||
setStatus(serviceStatus);
|
||||
setPlugins(pluginList);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleTogglePlugin = async (name: string, currentEnabled: boolean) => {
|
||||
try {
|
||||
if (currentEnabled) {
|
||||
await invoke("disable_plugin", { name });
|
||||
} else {
|
||||
await invoke("enable_plugin", { name });
|
||||
}
|
||||
await fetchData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
};
|
||||
|
||||
const handleReloadPlugins = async () => {
|
||||
try {
|
||||
await invoke("reload_plugins");
|
||||
await fetchData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnloadPlugin = async (name: string) => {
|
||||
try {
|
||||
await invoke("unload_plugin", { name });
|
||||
await fetchData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "enabled":
|
||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||
case "disabled":
|
||||
return <PowerOff className="h-4 w-4 text-gray-400" />;
|
||||
case "error":
|
||||
return <AlertCircle className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return <Clock className="h-4 w-4 text-yellow-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case "enabled":
|
||||
return "已启用";
|
||||
case "disabled":
|
||||
return "已禁用";
|
||||
case "error":
|
||||
return "错误";
|
||||
case "loaded":
|
||||
return "已加载";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 状态概览 */}
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold flex items-center gap-2">
|
||||
<Puzzle className="h-4 w-4" />
|
||||
插件系统
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleReloadPlugins}
|
||||
className="p-1 hover:bg-muted rounded"
|
||||
title="重新加载插件"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-2 bg-red-50 text-red-600 rounded text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status && (
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{status.plugin_count}</div>
|
||||
<div className="text-xs text-muted-foreground">已加载插件</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div
|
||||
className="text-sm font-mono truncate"
|
||||
title={status.plugins_dir}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4 inline mr-1" />
|
||||
{status.plugins_dir.split("/").pop()}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">插件目录</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 插件列表 */}
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="p-4 border-b">
|
||||
<h4 className="font-semibold">已安装插件</h4>
|
||||
</div>
|
||||
|
||||
{plugins.length === 0 ? (
|
||||
<div className="p-8 text-center text-muted-foreground">
|
||||
<Puzzle className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>暂无已安装的插件</p>
|
||||
<p className="text-sm mt-1">将插件放入插件目录即可自动加载</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{plugins.map((plugin) => (
|
||||
<PluginItem
|
||||
key={plugin.name}
|
||||
plugin={plugin}
|
||||
expanded={expandedPlugin === plugin.name}
|
||||
onToggleExpand={() =>
|
||||
setExpandedPlugin(
|
||||
expandedPlugin === plugin.name ? null : plugin.name,
|
||||
)
|
||||
}
|
||||
onToggleEnabled={() =>
|
||||
handleTogglePlugin(plugin.name, plugin.config.enabled)
|
||||
}
|
||||
onUnload={() => handleUnloadPlugin(plugin.name)}
|
||||
getStatusIcon={getStatusIcon}
|
||||
getStatusText={getStatusText}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PluginItemProps {
|
||||
plugin: PluginInfo;
|
||||
expanded: boolean;
|
||||
onToggleExpand: () => void;
|
||||
onToggleEnabled: () => void;
|
||||
onUnload: () => void;
|
||||
getStatusIcon: (status: string) => React.ReactNode;
|
||||
getStatusText: (status: string) => string;
|
||||
}
|
||||
|
||||
function PluginItem({
|
||||
plugin,
|
||||
expanded,
|
||||
onToggleExpand,
|
||||
onToggleEnabled,
|
||||
onUnload,
|
||||
getStatusIcon,
|
||||
getStatusText,
|
||||
}: PluginItemProps) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onToggleExpand}
|
||||
className="p-1 hover:bg-muted rounded"
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{plugin.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
v{plugin.version}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-xs">
|
||||
{getStatusIcon(plugin.status)}
|
||||
{getStatusText(plugin.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{plugin.description || "无描述"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onToggleEnabled}
|
||||
className={`p-2 rounded ${
|
||||
plugin.config.enabled
|
||||
? "bg-green-100 text-green-600 hover:bg-green-200"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
title={plugin.config.enabled ? "禁用插件" : "启用插件"}
|
||||
>
|
||||
{plugin.config.enabled ? (
|
||||
<Power className="h-4 w-4" />
|
||||
) : (
|
||||
<PowerOff className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={onUnload}
|
||||
className="p-2 rounded bg-red-100 text-red-600 hover:bg-red-200"
|
||||
title="卸载插件"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-4 pl-8 space-y-3">
|
||||
{plugin.author && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">作者:</span>
|
||||
{plugin.author}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">路径:</span>
|
||||
<span className="font-mono text-xs">{plugin.path}</span>
|
||||
</div>
|
||||
|
||||
{plugin.hooks.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">钩子:</span>
|
||||
<div className="flex gap-1 mt-1">
|
||||
{plugin.hooks.map((hook) => (
|
||||
<span
|
||||
key={hook}
|
||||
className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs"
|
||||
>
|
||||
{hook}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">统计:</span>
|
||||
<div className="grid grid-cols-3 gap-2 mt-1">
|
||||
<div className="text-center p-2 bg-muted rounded">
|
||||
<div className="font-bold">{plugin.state.execution_count}</div>
|
||||
<div className="text-xs text-muted-foreground">执行次数</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-muted rounded">
|
||||
<div className="font-bold text-red-500">
|
||||
{plugin.state.error_count}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">错误次数</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-muted rounded">
|
||||
<div className="font-bold">{plugin.config.timeout_ms}ms</div>
|
||||
<div className="text-xs text-muted-foreground">超时时间</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{plugin.state.last_error && (
|
||||
<div className="text-sm p-2 bg-red-50 text-red-600 rounded">
|
||||
<span className="font-medium">最后错误:</span>
|
||||
{plugin.state.last_error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PluginManager;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { PluginManager } from "./PluginManager";
|
||||
export { default } from "./PluginManager";
|
||||
Reference in New Issue
Block a user