diff --git a/astrbot/rust/_core.pyi b/astrbot/rust/_core.pyi new file mode 100644 index 000000000..c769f3858 --- /dev/null +++ b/astrbot/rust/_core.pyi @@ -0,0 +1,16 @@ +from typing import Any + +class AstrbotOrchestrator: + def start(self) -> None: ... + def stop(self) -> None: ... + def is_running(self) -> bool: ... + def register_star(self, name: str, handler: str) -> None: ... + def unregister_star(self, name: str) -> None: ... + def list_stars(self) -> list[str]: ... + def record_activity(self) -> None: ... + def get_stats(self) -> dict[str, Any]: ... + def set_protocol_connected(self, protocol: str, connected: bool) -> None: ... + def get_protocol_status(self, protocol: str) -> dict[str, Any] | None: ... + +def get_orchestrator() -> AstrbotOrchestrator: ... +def cli(args: list[str]) -> None: ... diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 1e68aec65..720fce8bc 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -56,7 +56,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -67,7 +67,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -88,15 +88,22 @@ version = "4.25.0" dependencies = [ "anyhow", "async-trait", + "base64", "clap", + "dirs", + "futures-util", "pyo3", + "pyo3-async-runtimes", + "reqwest", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", + "tokio-tungstenite", "toml", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -110,12 +117,46 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bigdecimal" version = "0.4.10" @@ -135,6 +176,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -154,15 +204,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -224,18 +288,82 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "deranged" version = "0.5.8" @@ -245,12 +373,69 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -264,7 +449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -283,6 +468,163 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -295,6 +637,108 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -319,6 +763,114 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indenter" version = "0.3.4" @@ -332,7 +884,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +dependencies = [ + "memchr", + "serde", ] [[package]] @@ -359,7 +929,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -388,6 +958,60 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -404,6 +1028,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" @@ -416,6 +1046,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.14" @@ -431,6 +1076,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -446,6 +1097,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.1.1" @@ -454,7 +1111,7 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -463,7 +1120,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -487,9 +1144,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -533,10 +1190,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "ordered-float" -version = "5.1.0" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0218004a4aae742209bee9c3cef05672f6b2708be36a50add8eb613b1f2a4008" dependencies = [ "num-traits", ] @@ -564,6 +1233,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "phf" version = "0.12.1" @@ -588,6 +1263,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -603,12 +1284,40 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -631,7 +1340,7 @@ dependencies = [ "chrono-tz", "either", "eyre", - "hashbrown", + "hashbrown 0.16.1", "iana-time-zone", "indexmap", "jiff", @@ -655,6 +1364,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "pyo3-async-runtimes" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7364a95bf00e8377bbf9b0f09d7ff9715a29d8fcf93b47d1a967363b973178" +dependencies = [ + "futures-channel", + "futures-util", + "once_cell", + "pin-project-lite", + "pyo3", +] + [[package]] name = "pyo3-build-config" version = "0.28.2" @@ -699,6 +1421,62 @@ dependencies = [ "syn", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -708,6 +1486,47 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -717,6 +1536,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -734,6 +1564,60 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rust_decimal" version = "1.40.0" @@ -744,18 +1628,146 @@ dependencies = [ "num-traits", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -808,6 +1820,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -839,6 +1862,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -852,15 +1881,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -872,19 +1913,80 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "target-lexicon" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -925,6 +2027,31 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -939,7 +2066,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -953,6 +2080,41 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "1.1.0+spec-1.1.0" @@ -992,6 +2154,51 @@ version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -1053,12 +2260,70 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.18", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1071,6 +2336,7 @@ version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -1081,12 +2347,55 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -1100,6 +2409,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -1132,6 +2455,78 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1173,6 +2568,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -1191,6 +2597,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1200,12 +2633,395 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index de227605a..cd747d338 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,12 +4,14 @@ version = "4.25.0" edition = "2024" [lib] -name = "astrbot__core" +name = "astrbot_core" crate-type = ["cdylib", "rlib"] [features] default = ["python"] -python = ["pyo3"] +python = ["pyo3", "pyo3-asyncio"] +pyo3-asyncio = [] +pyo3-async-runtimes = ["dep:pyo3-async-runtimes"] [dependencies] serde = { version = "1", features = ["derive"] } @@ -27,6 +29,13 @@ pyo3 = { version = "0.28", features = [ "full", "extension-module", ], optional = true } +pyo3-async-runtimes = { version = "0.28", optional = true } +uuid = { version = "1.22.0", features = ["v4"] } +reqwest = { version = "0.13.2", features = ["json", "default"] } +dirs = "6.0.0" +tokio-tungstenite = "0.29.0" +base64 = "0.22.1" +futures-util = "0.3" [profile.release] opt-level = 3 diff --git a/rust/src/a2a.rs b/rust/src/a2a.rs new file mode 100644 index 000000000..b9d0f587d --- /dev/null +++ b/rust/src/a2a.rs @@ -0,0 +1,464 @@ +//! A2A (Agent-to-Agent) Protocol Implementation +//! +//! A2A is an open protocol by Google Cloud for inter-agent communication. +//! This module implements the A2A client for AstrBot. +//! +//! Reference: https://google-a2a.github.io/ + +use crate::error::AstrBotError; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// ============================================================================ +// A2A Data Models +// ============================================================================ + +/// Agent Card - describes agent capabilities for service discovery +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentCard { + pub name: String, + pub description: Option, + pub version: String, + pub provider: Option, + pub capabilities: AgentCapabilities, + pub skills: Vec, + #[serde(default)] + pub security_schemes: HashMap, + pub url: String, +} + +/// Provider information +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentProvider { + pub organization: String, + pub url: Option, +} + +/// Agent capabilities +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AgentCapabilities { + #[serde(default)] + pub streaming: bool, + #[serde(default)] + pub push_notifications: bool, + #[serde(default)] + pub extensions: Vec, +} + +/// Agent skill +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentSkill { + pub id: String, + pub name: String, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub examples: Vec, +} + +/// Skill example +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillExample { + pub user: String, + pub agent: String, +} + +/// Security scheme +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SecurityScheme { + #[serde(rename = "type")] + pub scheme_type: String, + #[serde(default)] + pub flows: Vec, + #[serde(default)] + pub authorization_url: Option, +} + +/// Task status +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskStatus { + pub state: TaskState, + pub timestamp: String, +} + +/// Task states +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TaskState { + Submitted, + Working, + Completed, + Failed, + Canceled, + Rejected, + InputRequired, + AuthRequired, +} + +/// Task representation +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Task { + pub id: String, + #[serde(default)] + pub context_id: Option, + pub status: TaskStatus, + #[serde(default)] + pub artifacts: Vec, + #[serde(default)] + pub history: Vec, + #[serde(default)] + pub metadata: HashMap, +} + +/// Artifact - immutable output from agent +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Artifact { + #[serde(default)] + pub artifact_id: Option, + pub name: Option, + pub parts: Vec, +} + +/// Message part - atomic content unit +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] +pub enum Part { + Text { text: String }, + File { file: FilePart }, + Data { data: serde_json::Value }, +} + +/// File part +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FilePart { + pub name: String, + pub uri: String, +} + +/// Message between client and agent +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Message { + #[serde(default)] + pub message_id: Option, + pub role: MessageRole, + pub parts: Vec, + #[serde(default)] + pub reference_task_ids: Vec, +} + +/// Message role +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum MessageRole { + User, + Agent, +} + +/// A2A Response +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct A2AResponse { + #[serde(default)] + pub task: Option, + #[serde(default)] + pub message: Option, +} + +// ============================================================================ +// A2A Client +// ============================================================================ + +/// A2A Client for connecting to remote agents +#[derive(Debug)] +pub struct A2AClient { + /// Base URL of the A2A server + server_url: String, + /// Authentication header value + auth_header: Option, + /// HTTP client + client: Option, + /// Task history + tasks: HashMap, +} + +impl A2AClient { + /// Create a new A2A client + pub fn new(server_url: &str) -> Self { + Self { + server_url: server_url.to_string(), + auth_header: None, + client: None, + tasks: HashMap::new(), + } + } + + /// Set authentication + pub fn with_auth(mut self, auth_header: &str) -> Self { + self.auth_header = Some(auth_header.to_string()); + self + } + + /// Initialize the client + pub async fn connect(&mut self) -> Result<(), AstrBotError> { + self.client = Some(reqwest::Client::new()); + + tracing::info!("A2A client connected to {}", self.server_url); + Ok(()) + } + + /// Fetch agent card for service discovery + pub async fn get_agent_card(&self) -> Result { + let client = self + .client + .as_ref() + .ok_or_else(|| AstrBotError::NotConnected("A2A client not connected".into()))?; + + let url = format!("{}/.well-known/agent-card.json", self.server_url); + let response = client.get(&url).send().await.map_err(|e| { + AstrBotError::ConnectionFailed(format!("Failed to fetch agent card: {}", e)) + })?; + + let agent_card: AgentCard = response + .json() + .await + .map_err(|e| AstrBotError::Protocol(format!("Failed to parse agent card: {}", e)))?; + + Ok(agent_card) + } + + /// Send a message to an agent + pub async fn send_message( + &mut self, + message: Message, + task_id: Option<&str>, + session_id: Option<&str>, + ) -> Result { + let client = self + .client + .as_ref() + .ok_or_else(|| AstrBotError::NotConnected("A2A client not connected".into()))?; + + let request = A2ARequest::new( + &uuid::Uuid::new_v4().to_string(), + "SendMessage", + serde_json::to_value(SendMessageParams { + message, + task_id: task_id.map(String::from), + session_id: session_id.map(String::from), + }) + .map_err(|e| AstrBotError::Json(e))?, + ); + + let response = client + .post(&self.server_url) + .header("Content-Type", "application/json") + .header("A2A-Version", "1.0.0") + .json(&request) + .send() + .await + .map_err(|e| AstrBotError::ConnectionFailed(format!("SendMessage failed: {}", e)))?; + + let result: serde_json::Value = response + .json() + .await + .map_err(|e| AstrBotError::Protocol(format!("Failed to parse response: {}", e)))?; + + // Parse into A2AResponse + let a2a_response: A2AResponse = + serde_json::from_value(result).map_err(|e| AstrBotError::Json(e))?; + + Ok(a2a_response) + } + + /// Send a streaming message + pub async fn send_streaming_message( + &mut self, + message: Message, + ) -> Result { + let client = self + .client + .as_ref() + .ok_or_else(|| AstrBotError::NotConnected("A2A client not connected".into()))?; + + let request = A2ARequest::new( + &uuid::Uuid::new_v4().to_string(), + "SendStreamingMessage", + serde_json::to_value(SendStreamingMessageParams { message }) + .map_err(|e| AstrBotError::Json(e))?, + ); + + // For streaming, we return the task ID and let the caller handle SSE + let response = client + .post(&self.server_url) + .header("Content-Type", "application/json") + .header("A2A-Version", "1.0.0") + .json(&request) + .send() + .await + .map_err(|e| { + AstrBotError::ConnectionFailed(format!("SendStreamingMessage failed: {}", e)) + })?; + + // Parse streaming response to get task ID + let result: serde_json::Value = response.json().await.map_err(|e| { + AstrBotError::Protocol(format!("Failed to parse streaming response: {}", e)) + })?; + + let task_id = result + .get("task") + .and_then(|t| t.get("id")) + .and_then(|id| id.as_str()) + .map(String::from) + .ok_or_else(|| AstrBotError::Protocol("No task ID in streaming response".into()))?; + + Ok(task_id) + } + + /// Get task status and result + pub async fn get_task(&mut self, task_id: &str) -> Result { + let client = self + .client + .as_ref() + .ok_or_else(|| AstrBotError::NotConnected("A2A client not connected".into()))?; + + let request = A2ARequest::new( + &uuid::Uuid::new_v4().to_string(), + "GetTask", + serde_json::json!({ "id": task_id }), + ); + + let response = client + .post(&self.server_url) + .header("Content-Type", "application/json") + .header("A2A-Version", "1.0.0") + .json(&request) + .send() + .await + .map_err(|e| AstrBotError::ConnectionFailed(format!("GetTask failed: {}", e)))?; + + let result: serde_json::Value = response + .json() + .await + .map_err(|e| AstrBotError::Protocol(format!("Failed to parse task: {}", e)))?; + + let task: Task = serde_json::from_value(result).map_err(|e| AstrBotError::Json(e))?; + + self.tasks.insert(task.id.clone(), task.clone()); + + Ok(task) + } + + /// Cancel a task + pub async fn cancel_task(&mut self, task_id: &str) -> Result { + let client = self + .client + .as_ref() + .ok_or_else(|| AstrBotError::NotConnected("A2A client not connected".into()))?; + + let request = A2ARequest::new( + &uuid::Uuid::new_v4().to_string(), + "CancelTask", + serde_json::json!({ "id": task_id }), + ); + + let response = client + .post(&self.server_url) + .header("Content-Type", "application/json") + .header("A2A-Version", "1.0.0") + .json(&request) + .send() + .await + .map_err(|e| AstrBotError::ConnectionFailed(format!("CancelTask failed: {}", e)))?; + + let result: serde_json::Value = response + .json() + .await + .map_err(|e| AstrBotError::Protocol(format!("Failed to parse canceled task: {}", e)))?; + + let task: Task = serde_json::from_value(result).map_err(|e| AstrBotError::Json(e))?; + + Ok(task) + } + + /// Check if agent is connected + pub fn is_connected(&self) -> bool { + self.client.is_some() + } +} + +// ============================================================================ +// A2A Request/Response Types +// ============================================================================ + +#[derive(Debug, Serialize, Deserialize)] +struct A2ARequest { + #[serde(rename = "jsonrpc")] + json_rpc: String, + id: String, + method: String, + params: serde_json::Value, +} + +impl A2ARequest { + fn new(id: &str, method: &str, params: serde_json::Value) -> Self { + Self { + json_rpc: "2.0".to_string(), + id: id.to_string(), + method: method.to_string(), + params, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SendMessageParams { + message: Message, + #[serde(skip_serializing_if = "Option::is_none")] + task_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + session_id: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SendStreamingMessageParams { + message: Message, +} + +// ============================================================================ +// Protocol Client Trait Implementation +// ============================================================================ + +#[async_trait] +impl crate::protocol::ProtocolClient for A2AClient { + fn name(&self) -> &'static str { + "a2a" + } + + fn is_connected(&self) -> bool { + self.is_connected() + } + + async fn connect(&mut self) -> Result<(), AstrBotError> { + A2AClient::connect(self).await + } + + async fn disconnect(&mut self) -> Result<(), AstrBotError> { + self.client = None; + self.tasks.clear(); + tracing::info!("A2A client disconnected"); + Ok(()) + } +} diff --git a/rust/src/abp.rs b/rust/src/abp.rs new file mode 100644 index 000000000..69e61d2fd --- /dev/null +++ b/rust/src/abp.rs @@ -0,0 +1,726 @@ +//! ABP (AstrBot Plugin) Protocol Implementation +//! +//! ABP is the plugin communication protocol for AstrBot, supporting: +//! - In-process and out-of-process loading modes +//! - Full plugin lifecycle management +//! - Tool calling, message handling, event subscriptions +//! - JSON-RPC 2.0 communication +//! +//! Reference: openspec/abp.md + +use crate::error::AstrBotError; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::UnixStream; + +// ============================================================================ +// ABP Data Models +// ============================================================================ + +/// Plugin loading mode +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PluginLoadMode { + /// In-process: direct function calls, zero serialization overhead + #[default] + InProcess, + /// Out-of-process: separate process, JSON-RPC over Unix Socket/HTTP + OutOfProcess, +} + +impl std::fmt::Display for PluginLoadMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PluginLoadMode::InProcess => write!(f, "in_process"), + PluginLoadMode::OutOfProcess => write!(f, "out_of_process"), + } + } +} + +/// Plugin transport type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PluginTransport { + /// Stdio transport (for subprocess) + #[default] + Stdio, + /// Unix Socket transport + UnixSocket, + /// HTTP transport + Http, +} + +impl std::fmt::Display for PluginTransport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PluginTransport::Stdio => write!(f, "stdio"), + PluginTransport::UnixSocket => write!(f, "unix_socket"), + PluginTransport::Http => write!(f, "http"), + } + } +} + +/// Plugin configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginConfig { + pub name: String, + #[serde(default)] + pub version: String, + #[serde(default)] + pub load_mode: PluginLoadMode, + #[serde(default)] + pub command: Option, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub env: HashMap, + #[serde(default)] + pub transport: PluginTransport, + #[serde(default)] + pub url: Option, +} + +impl Default for PluginConfig { + fn default() -> Self { + Self { + name: String::new(), + version: "1.0.0".to_string(), + load_mode: PluginLoadMode::InProcess, + command: None, + args: Vec::new(), + env: HashMap::new(), + transport: PluginTransport::Stdio, + url: None, + } + } +} + +/// Plugin capabilities +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginCapabilities { + #[serde(default)] + pub tools: bool, + #[serde(default)] + pub handlers: bool, + #[serde(default)] + pub events: bool, + #[serde(default)] + pub resources: bool, +} + +/// Plugin metadata +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginMetadata { + #[serde(default)] + pub display_name: Option, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub author: Option, + #[serde(default)] + pub homepage: Option, + #[serde(default)] + pub support_platforms: Vec, + #[serde(default)] + pub astrbot_version: Option, +} + +/// Initialize result from plugin +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InitializeResult { + pub protocol_version: String, + pub server_info: ServerInfo, + pub capabilities: PluginCapabilities, + #[serde(default)] + pub metadata: Option, +} + +/// Server info +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerInfo { + pub name: String, + pub version: String, +} + +/// Tool definition +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Tool { + pub name: String, + #[serde(default)] + pub description: String, + #[serde(default)] + pub input_schema: serde_json::Value, +} + +/// Tool call result +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolResult { + #[serde(default)] + pub content: Vec, +} + +/// Tool content +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolContent { + #[serde(rename = "type")] + pub content_type: String, + pub text: Option, +} + +/// Message event +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageEvent { + pub message_id: String, + pub unified_msg_origin: String, + pub message_str: String, + pub sender: SenderInfo, + pub message_chain: Vec, +} + +/// Sender info +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SenderInfo { + pub user_id: String, + #[serde(default)] + pub nickname: String, +} + +/// Message chain item +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageChainItem { + #[serde(rename = "type")] + pub item_type: String, + #[serde(default)] + pub text: Option, +} + +/// Handle event result +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HandleEventResult { + pub handled: bool, + #[serde(default)] + pub results: Vec, + #[serde(default)] + pub stop_propagation: bool, +} + +/// Event notification +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EventNotification { + pub event_type: String, + #[serde(default)] + pub data: serde_json::Value, +} + +// ============================================================================ +// ABP Client +// ============================================================================ + +/// ABP Client for managing plugins +#[derive(Debug)] +pub struct AbpClient { + /// Connected state + connected: bool, + /// In-process plugins registry (name -> handler) + in_process_plugins: HashMap, + /// Out-of-process plugins registry (name -> OOP client) + out_of_process_plugins: HashMap, +} + +/// In-process plugin handler +#[derive(Debug, Clone)] +pub struct InProcessPlugin { + pub config: PluginConfig, + pub capabilities: PluginCapabilities, + pub metadata: Option, + pub tools: Vec, +} + +/// Out-of-process plugin client +#[derive(Debug)] +pub struct OutOfProcessPlugin { + pub config: PluginConfig, + pub capabilities: PluginCapabilities, + pub metadata: Option, + pub tools: Vec, + pub socket_path: Option, + pub http_url: Option, +} + +impl AbpClient { + /// Create a new ABP client + pub fn new() -> Self { + Self { + connected: false, + in_process_plugins: HashMap::new(), + out_of_process_plugins: HashMap::new(), + } + } + + /// Connect the ABP client + pub async fn connect(&mut self) -> Result<(), AstrBotError> { + self.connected = true; + tracing::info!( + "ABP client connected (in_process: {}, out_of_process: {})", + self.in_process_plugins.len(), + self.out_of_process_plugins.len() + ); + Ok(()) + } + + /// Disconnect the ABP client + pub async fn disconnect(&mut self) -> Result<(), AstrBotError> { + self.connected = false; + self.in_process_plugins.clear(); + self.out_of_process_plugins.clear(); + tracing::info!("ABP client disconnected"); + Ok(()) + } + + /// Check if connected + pub fn is_connected(&self) -> bool { + self.connected + } + + /// Set connected state (sync version for orchestrator) + pub fn set_connected(&mut self, connected: bool) { + self.connected = connected; + } + + /// Set connected state without consuming self + pub fn set_connected_ref(&mut self, connected: bool) { + self.connected = connected; + } + /// Register an in-process plugin + pub fn register_in_process_plugin(&mut self, config: PluginConfig) { + let plugin = InProcessPlugin { + config: config.clone(), + capabilities: PluginCapabilities::default(), + metadata: None, + tools: Vec::new(), + }; + tracing::debug!("Registered in-process plugin: {}", config.name); + self.in_process_plugins.insert(config.name, plugin); + } + + /// Register an out-of-process plugin + pub fn register_out_of_process_plugin(&mut self, config: PluginConfig) { + let socket_path = if config.transport == PluginTransport::UnixSocket { + Some(PathBuf::from(format!( + "/tmp/astrbot_plugin_{}.sock", + config.name + ))) + } else { + None + }; + + let plugin = OutOfProcessPlugin { + config: config.clone(), + capabilities: PluginCapabilities::default(), + metadata: None, + tools: Vec::new(), + socket_path, + http_url: config.url.clone(), + }; + tracing::debug!("Registered out-of-process plugin: {}", config.name); + self.out_of_process_plugins.insert(config.name, plugin); + } + + /// Unregister a plugin + pub fn unregister_plugin(&mut self, name: &str) { + self.in_process_plugins.remove(name); + self.out_of_process_plugins.remove(name); + tracing::debug!("Unregistered plugin: {}", name); + } + + /// List all registered plugin names + pub fn list_plugins(&self) -> Vec { + let mut names: Vec = self.in_process_plugins.keys().cloned().collect(); + names.extend(self.out_of_process_plugins.keys().cloned()); + names.sort(); + names.dedup(); + names + } + + /// Get plugin info + pub fn get_plugin_info(&self, name: &str) -> Option { + if let Some(plugin) = self.in_process_plugins.get(name) { + Some(PluginInfo { + name: plugin.config.name.clone(), + version: plugin.config.version.clone(), + load_mode: plugin.config.load_mode, + capabilities: plugin.capabilities.clone(), + metadata: plugin.metadata.clone(), + tools_count: plugin.tools.len(), + }) + } else if let Some(plugin) = self.out_of_process_plugins.get(name) { + Some(PluginInfo { + name: plugin.config.name.clone(), + version: plugin.config.version.clone(), + load_mode: plugin.config.load_mode, + capabilities: plugin.capabilities.clone(), + metadata: plugin.metadata.clone(), + tools_count: plugin.tools.len(), + }) + } else { + None + } + } + + /// Call a plugin tool (out-of-process) + pub async fn call_tool( + &mut self, + plugin_name: &str, + tool_name: &str, + arguments: serde_json::Value, + ) -> Result { + // Extract transport and connection info first to avoid borrow conflict + let transport = { + let plugin = self + .out_of_process_plugins + .get(plugin_name) + .ok_or_else(|| { + AstrBotError::NotFound(format!("Plugin '{}' not found", plugin_name)) + })?; + plugin.config.transport + }; + + match transport { + PluginTransport::UnixSocket => { + let socket_path = { + let plugin = self + .out_of_process_plugins + .get(plugin_name) + .ok_or_else(|| { + AstrBotError::NotFound(format!("Plugin '{}' not found", plugin_name)) + })?; + plugin.socket_path.clone() + }; + self.call_tool_unix_socket(&socket_path, tool_name, arguments) + .await + } + PluginTransport::Http => { + let http_url = { + let plugin = self + .out_of_process_plugins + .get(plugin_name) + .ok_or_else(|| { + AstrBotError::NotFound(format!("Plugin '{}' not found", plugin_name)) + })?; + plugin.http_url.clone() + }; + self.call_tool_http(&http_url, tool_name, arguments).await + } + PluginTransport::Stdio => Err(AstrBotError::Protocol( + "Stdio transport not implemented for tool calls".into(), + )), + } + } + + /// Call tool via Unix Socket + async fn call_tool_unix_socket( + &self, + socket_path: &Option, + tool_name: &str, + arguments: serde_json::Value, + ) -> Result { + let socket_path = socket_path + .as_ref() + .ok_or_else(|| AstrBotError::InvalidState("No socket path configured".into()))?; + + let mut stream = UnixStream::connect(socket_path).await.map_err(|e| { + AstrBotError::ConnectionFailed(format!("Unix socket connect failed: {}", e)) + })?; + + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": uuid::Uuid::new_v4().to_string(), + "method": "tools/call", + "params": { + "name": tool_name, + "arguments": arguments + } + }); + + let content = serde_json::to_string(&request).map_err(|e| AstrBotError::Json(e))?; + let header = format!("Content-Length: {}\r\n\r\n", content.len()); + + stream.write_all(header.as_bytes()).await?; + stream.write_all(content.as_bytes()).await?; + + // Read response + let mut buffer = Vec::new(); + stream.read_to_end(&mut buffer).await?; + + let response: serde_json::Value = + serde_json::from_slice(&buffer).map_err(|e| AstrBotError::Json(e))?; + + if let Some(error) = response.get("error") { + return Err(AstrBotError::Protocol(error.to_string())); + } + + let result: ToolResult = + serde_json::from_value(response.get("result").cloned().unwrap_or_default()) + .map_err(|e| AstrBotError::Json(e))?; + + Ok(result) + } + + /// Call tool via HTTP + async fn call_tool_http( + &self, + http_url: &Option, + tool_name: &str, + arguments: serde_json::Value, + ) -> Result { + let url = http_url + .as_ref() + .ok_or_else(|| AstrBotError::InvalidState("No HTTP URL configured".into()))?; + + let client = reqwest::Client::new(); + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": uuid::Uuid::new_v4().to_string(), + "method": "tools/call", + "params": { + "name": tool_name, + "arguments": arguments + } + }); + + let response = client + .post(url) + .json(&request) + .send() + .await + .map_err(|e| AstrBotError::ConnectionFailed(e.to_string()))?; + + let result: ToolResult = response + .json() + .await + .map_err(|e| AstrBotError::Protocol(e.to_string()))?; + + Ok(result) + } + + /// Handle an event with a plugin + pub async fn handle_event( + &mut self, + plugin_name: &str, + event_type: &str, + event: serde_json::Value, + ) -> Result { + // Extract transport first to avoid borrow conflict + let transport = { + let plugin = self + .out_of_process_plugins + .get(plugin_name) + .ok_or_else(|| { + AstrBotError::NotFound(format!("Plugin '{}' not found", plugin_name)) + })?; + plugin.config.transport + }; + + match transport { + PluginTransport::UnixSocket => { + let socket_path = { + let plugin = self + .out_of_process_plugins + .get(plugin_name) + .ok_or_else(|| { + AstrBotError::NotFound(format!("Plugin '{}' not found", plugin_name)) + })?; + plugin.socket_path.clone() + }; + self.handle_event_unix_socket(&socket_path, event_type, event) + .await + } + PluginTransport::Http => { + let http_url = { + let plugin = self + .out_of_process_plugins + .get(plugin_name) + .ok_or_else(|| { + AstrBotError::NotFound(format!("Plugin '{}' not found", plugin_name)) + })?; + plugin.http_url.clone() + }; + self.handle_event_http(&http_url, event_type, event).await + } + PluginTransport::Stdio => Err(AstrBotError::Protocol( + "Stdio transport not implemented for events".into(), + )), + } + } + + /// Handle event via Unix Socket + async fn handle_event_unix_socket( + &self, + socket_path: &Option, + event_type: &str, + event: serde_json::Value, + ) -> Result { + let socket_path = socket_path + .as_ref() + .ok_or_else(|| AstrBotError::InvalidState("No socket path configured".into()))?; + + let mut stream = UnixStream::connect(socket_path).await.map_err(|e| { + AstrBotError::ConnectionFailed(format!("Unix socket connect failed: {}", e)) + })?; + + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": uuid::Uuid::new_v4().to_string(), + "method": "plugin.handle_event", + "params": { + "event_type": event_type, + "event": event + } + }); + + let content = serde_json::to_string(&request).map_err(|e| AstrBotError::Json(e))?; + let header = format!("Content-Length: {}\r\n\r\n", content.len()); + + stream.write_all(header.as_bytes()).await?; + stream.write_all(content.as_bytes()).await?; + + // Read response + let mut buffer = Vec::new(); + stream.read_to_end(&mut buffer).await?; + + let response: serde_json::Value = + serde_json::from_slice(&buffer).map_err(|e| AstrBotError::Json(e))?; + + if let Some(error) = response.get("error") { + return Err(AstrBotError::Protocol(error.to_string())); + } + + let result: HandleEventResult = + serde_json::from_value(response.get("result").cloned().unwrap_or_default()) + .map_err(|e| AstrBotError::Json(e))?; + + Ok(result) + } + + /// Handle event via HTTP + async fn handle_event_http( + &self, + http_url: &Option, + event_type: &str, + event: serde_json::Value, + ) -> Result { + let url = http_url + .as_ref() + .ok_or_else(|| AstrBotError::InvalidState("No HTTP URL configured".into()))?; + + let client = reqwest::Client::new(); + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": uuid::Uuid::new_v4().to_string(), + "method": "plugin.handle_event", + "params": { + "event_type": event_type, + "event": event + } + }); + + let response = client + .post(url) + .json(&request) + .send() + .await + .map_err(|e| AstrBotError::ConnectionFailed(e.to_string()))?; + + let result: HandleEventResult = response + .json() + .await + .map_err(|e| AstrBotError::Protocol(e.to_string()))?; + + Ok(result) + } + + /// Get plugin tools + pub fn get_plugin_tools(&self, name: &str) -> Option> { + if let Some(plugin) = self.in_process_plugins.get(name) { + Some(plugin.tools.clone()) + } else if let Some(plugin) = self.out_of_process_plugins.get(name) { + Some(plugin.tools.clone()) + } else { + None + } + } + + /// Health check for a plugin + pub fn health_check(&self, name: &str) -> bool { + if self.in_process_plugins.contains_key(name) { + true + } else if let Some(plugin) = self.out_of_process_plugins.get(name) { + if let Some(socket_path) = &plugin.socket_path { + socket_path.exists() + } else if plugin.http_url.is_some() { + true // HTTP plugins are assumed healthy if URL is configured + } else { + false + } + } else { + false + } + } +} + +impl Default for AbpClient { + fn default() -> Self { + Self::new() + } +} + +/// Plugin info for listing +#[derive(Debug, Clone)] +pub struct PluginInfo { + pub name: String, + pub version: String, + pub load_mode: PluginLoadMode, + pub capabilities: PluginCapabilities, + pub metadata: Option, + pub tools_count: usize, +} + +// ============================================================================ +// Protocol Client Trait Implementation +// ============================================================================ + +use async_trait::async_trait; + +#[async_trait] +impl crate::protocol::ProtocolClient for AbpClient { + fn name(&self) -> &'static str { + "abp" + } + + fn is_connected(&self) -> bool { + self.is_connected() + } + + async fn connect(&mut self) -> Result<(), AstrBotError> { + self.connect().await + } + + async fn disconnect(&mut self) -> Result<(), AstrBotError> { + self.disconnect().await + } +} diff --git a/rust/src/cli.rs b/rust/src/cli.rs index 222112c1a..9bf04496d 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -1,7 +1,7 @@ //! AstrBot Core CLI use anyhow::Result; -use clap::{Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand}; #[derive(Parser)] #[command(name = "astrbot-rs")] @@ -13,7 +13,7 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { - /// Start the AstrBot core runtime + /// Start the AstrBot Core runtime Start { /// Host to bind to #[arg(long, default_value = "127.0.0.1")] @@ -34,7 +34,64 @@ pub fn cli() -> Result<()> { } pub fn cli_with_args(args: &[String]) -> Result<()> { - let cli = Cli::try_parse_from(args).map_err(|e| anyhow::anyhow!("{e}"))?; + // Handle --help and --version explicitly + if args.iter().any(|a| a == "--help" || a == "-h") { + let mut cmd = Cli::command(); + cmd.print_help()?; + println!(); + return Ok(()); + } + if args.iter().any(|a| a == "--version" || a == "-V") { + println!("astrbot-rs {}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + + // Handle empty args - show help + if args.is_empty() { + let mut cmd = Cli::command(); + cmd.print_help()?; + println!(); + return Ok(()); + } + + // When called from Python with sys.argv, clap treats the first element as bin_name. + // We need to handle two cases: + // 1. Single element that's a known subcommand (e.g., ['stats']) - prepend bin_name + // 2. Multiple elements where first is program name (e.g., ['/path/astrbot-rs', 'stats']) - prepend bin_name + let known_subcommands = ["start", "stats", "health", "help"]; + let first_is_subcommand = args.len() == 1 && known_subcommands.contains(&args[0].as_str()); + let first_is_program_name = !args[0].starts_with('-'); + + let parse_args: Vec<&str> = if first_is_subcommand { + // Case 1: ['stats'] -> prepend bin_name + vec!["astrbot-rs", args[0].as_str()] + } else if first_is_program_name { + // Case 2: ['/path/astrbot-rs', 'stats', ...] -> strip first, prepend bin_name + if args.len() > 1 { + let mut prefixed = vec!["astrbot-rs"]; + prefixed.extend(args[1..].iter().map(|s| s.as_str())); + prefixed + } else { + // Only program name, no subcommand - show help + let mut cmd = Cli::command(); + cmd.print_help()?; + println!(); + return Ok(()); + } + } else { + // Normal case: args start with flags + args.iter().map(|s| s.as_str()).collect() + }; + + // If all args consumed, show help + if parse_args.is_empty() { + let mut cmd = Cli::command(); + cmd.print_help()?; + println!(); + return Ok(()); + } + + let cli = Cli::try_parse_from(parse_args).map_err(|e| anyhow::anyhow!("{e}"))?; match cli.command { Commands::Start { host, port } => { diff --git a/rust/src/config.rs b/rust/src/config.rs index d836428c4..2d84352c1 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -1,132 +1,1098 @@ //! Configuration management for AstrBot Core +//! +//! Configuration follows these principles: +//! - Separation of concerns: configs organized by functional domain +//! - Sensitive info isolation: keys stored in separate secrets file +//! - Environment variable priority: keys injected via env vars +//! - Layered config: system -> platform -> provider -> agent -> plugins +//! +//! Reference: openspec/config.md use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::path::PathBuf; -/// AstrBot Core configuration -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct Config { - /// Runtime settings - pub runtime: RuntimeConfig, - /// Protocol client settings - pub protocols: ProtocolsConfig, - /// Logging settings - pub logging: LoggingConfig, -} +// ============================================================================ +// Main Config +// ============================================================================ +/// Main configuration entry #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RuntimeConfig { - /// Whether the runtime is in debug mode - pub debug: bool, - /// Data directory path - pub data_dir: PathBuf, - /// Config directory path - pub config_dir: PathBuf, - /// Temporary directory path - pub temp_dir: PathBuf, +pub struct AstrBotConfig { + #[serde(default)] + pub system: SystemConfig, + #[serde(default)] + pub platform: PlatformConfig, + #[serde(default)] + pub providers: ProvidersConfig, + #[serde(default)] + pub agent: AgentConfig, + #[serde(default)] + pub plugins: HashMap, } -impl Default for RuntimeConfig { +impl Default for AstrBotConfig { fn default() -> Self { Self { - debug: false, - data_dir: PathBuf::from("~/.astrbot/data"), - config_dir: PathBuf::from("~/.astrbot/config"), - temp_dir: PathBuf::from("~/.astrbot/temp"), + system: SystemConfig::default(), + platform: PlatformConfig::default(), + providers: ProvidersConfig::default(), + agent: AgentConfig::default(), + plugins: HashMap::new(), } } } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct ProtocolsConfig { - /// LSP protocol settings - pub lsp: ProtocolSettings, - /// MCP protocol settings - pub mcp: ProtocolSettings, - /// ACP protocol settings - pub acp: ProtocolSettings, - /// ABP protocol settings - pub abp: ProtocolSettings, -} +impl AstrBotConfig { + /// Load all config files from config directory + pub fn load_from_dir(config_dir: &PathBuf) -> anyhow::Result { + let mut config = AstrBotConfig::default(); -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProtocolSettings { - /// Whether this protocol is enabled - pub enabled: bool, - /// Connection timeout in seconds - pub timeout_secs: u64, - /// Retry settings - pub retry: RetrySettings, -} - -impl Default for ProtocolSettings { - fn default() -> Self { - Self { - enabled: true, - timeout_secs: 30, - retry: RetrySettings::default(), + let system_path = config_dir.join("system.yaml"); + if system_path.exists() { + let content = std::fs::read_to_string(&system_path)?; + config.system = toml::from_str(&content)?; } - } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RetrySettings { - /// Maximum number of retries - pub max_retries: u32, - /// Initial backoff delay in milliseconds - pub initial_delay_ms: u64, - /// Maximum backoff delay in milliseconds - pub max_delay_ms: u64, -} - -impl Default for RetrySettings { - fn default() -> Self { - Self { - max_retries: 3, - initial_delay_ms: 100, - max_delay_ms: 5000, + let platform_path = config_dir.join("platform.yaml"); + if platform_path.exists() { + let content = std::fs::read_to_string(&platform_path)?; + config.platform = toml::from_str(&content)?; } - } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LoggingConfig { - /// Log level (trace, debug, info, warn, error) - pub level: String, - /// Whether to use structured logging - pub structured: bool, - /// Log file path - pub file: Option, -} - -impl Default for LoggingConfig { - fn default() -> Self { - Self { - level: "info".to_string(), - structured: true, - file: None, + let providers_path = config_dir.join("providers.yaml"); + if providers_path.exists() { + let content = std::fs::read_to_string(&providers_path)?; + config.providers = toml::from_str(&content)?; + } + + let agent_path = config_dir.join("agent.yaml"); + if agent_path.exists() { + let content = std::fs::read_to_string(&agent_path)?; + config.agent = toml::from_str(&content)?; + } + + let plugins_dir = config_dir.join("plugins"); + if plugins_dir.is_dir() { + if let Ok(entries) = std::fs::read_dir(plugins_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("yaml") { + if let Some(name) = path.file_stem().and_then(|s| s.to_str()) { + if let Ok(content) = std::fs::read_to_string(&path) { + if let Ok(plugin_config) = + serde_json::from_str::(&content) + { + config.plugins.insert(name.to_string(), plugin_config); + } + } + } + } + } + } } - } -} -impl Config { - /// Load configuration from a file - pub fn load(path: &PathBuf) -> anyhow::Result { - let content = std::fs::read_to_string(path)?; - let config: Config = toml::from_str(&content)?; Ok(config) } +} - /// Save configuration to a file - pub fn save(&self, path: &PathBuf) -> anyhow::Result<()> { - let content = toml::to_string_pretty(self)?; - std::fs::write(path, content)?; - Ok(()) +// ============================================================================ +// System Config (system.yaml) +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemConfig { + #[serde(default)] + pub log: LogConfig, + #[serde(default)] + pub proxy: ProxyConfig, + #[serde(default)] + pub trace: TraceConfig, + #[serde(default)] + pub temp: TempConfig, + #[serde(default)] + pub timezone: String, + #[serde(default)] + pub pypi_index_url: String, + #[serde(default)] + pub pip_install_arg: String, + #[serde(default)] + pub callback_api_base: String, +} + +impl Default for SystemConfig { + fn default() -> Self { + Self { + log: LogConfig::default(), + proxy: ProxyConfig::default(), + trace: TraceConfig::default(), + temp: TempConfig::default(), + timezone: "Asia/Shanghai".to_string(), + pypi_index_url: String::new(), + pip_install_arg: String::new(), + callback_api_base: String::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LogConfig { + #[serde(default)] + pub level: String, + #[serde(default)] + pub file_enable: bool, + #[serde(default)] + pub file_path: String, + #[serde(default)] + pub file_max_mb: u32, + #[serde(default)] + pub disable_access_log: bool, +} + +impl Default for LogConfig { + fn default() -> Self { + Self { + level: "INFO".to_string(), + file_enable: false, + file_path: String::new(), + file_max_mb: 20, + disable_access_log: true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProxyConfig { + #[serde(default)] + pub http_proxy: String, + #[serde(default)] + pub https_proxy: String, + #[serde(default)] + pub no_proxy: Vec, +} + +impl Default for ProxyConfig { + fn default() -> Self { + Self { + http_proxy: String::new(), + https_proxy: String::new(), + no_proxy: vec!["localhost".to_string(), "127.0.0.1".to_string()], + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TraceConfig { + #[serde(default)] + pub enable: bool, + #[serde(default)] + pub log_enable: bool, + #[serde(default)] + pub log_path: String, +} + +impl Default for TraceConfig { + fn default() -> Self { + Self { + enable: false, + log_enable: false, + log_path: String::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TempConfig { + #[serde(default)] + pub dir_max_size: u64, +} + +impl Default for TempConfig { + fn default() -> Self { + Self { dir_max_size: 1024 } + } +} + +// ============================================================================ +// Platform Config (platform.yaml) +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlatformConfig { + #[serde(default)] + pub platform_settings: PlatformSettings, + #[serde(default)] + pub platforms: Vec, + #[serde(default)] + pub platform_specific: PlatformSpecificConfig, +} + +impl Default for PlatformConfig { + fn default() -> Self { + Self { + platform_settings: PlatformSettings::default(), + platforms: Vec::new(), + platform_specific: PlatformSpecificConfig::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlatformSettings { + #[serde(default)] + pub unique_session: bool, + #[serde(default)] + pub rate_limit: RateLimitConfig, + #[serde(default)] + pub reply_prefix: String, + #[serde(default)] + pub forward_threshold: u32, + #[serde(default)] + pub enable_id_white_list: bool, + #[serde(default)] + pub id_whitelist: Vec, + #[serde(default)] + pub id_whitelist_log: bool, + #[serde(default)] + pub wl_ignore_admin_on_group: bool, + #[serde(default)] + pub wl_ignore_admin_on_friend: bool, + #[serde(default)] + pub reply_with_mention: bool, + #[serde(default)] + pub reply_with_quote: bool, + #[serde(default)] + pub path_mapping: Vec, + #[serde(default)] + pub segmented_reply: SegmentedReplyConfig, + #[serde(default)] + pub no_permission_reply: bool, + #[serde(default)] + pub empty_mention_waiting: bool, + #[serde(default)] + pub empty_mention_waiting_need_reply: bool, + #[serde(default)] + pub friend_message_needs_wake_prefix: bool, + #[serde(default)] + pub ignore_bot_self_message: bool, + #[serde(default)] + pub ignore_at_all: bool, +} + +impl Default for PlatformSettings { + fn default() -> Self { + Self { + unique_session: false, + rate_limit: RateLimitConfig::default(), + reply_prefix: String::new(), + forward_threshold: 1500, + enable_id_white_list: true, + id_whitelist: Vec::new(), + id_whitelist_log: true, + wl_ignore_admin_on_group: true, + wl_ignore_admin_on_friend: true, + reply_with_mention: false, + reply_with_quote: false, + path_mapping: Vec::new(), + segmented_reply: SegmentedReplyConfig::default(), + no_permission_reply: true, + empty_mention_waiting: true, + empty_mention_waiting_need_reply: true, + friend_message_needs_wake_prefix: false, + ignore_bot_self_message: true, + ignore_at_all: false, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RateLimitConfig { + #[serde(default)] + pub time: u32, + #[serde(default)] + pub count: u32, + #[serde(default)] + pub strategy: String, +} + +impl Default for RateLimitConfig { + fn default() -> Self { + Self { + time: 60, + count: 30, + strategy: "stall".to_string(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PathMapping { + pub from: String, + pub to: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SegmentedReplyConfig { + #[serde(default)] + pub enable: bool, + #[serde(default)] + pub only_llm_result: bool, + #[serde(default)] + pub interval_method: String, + #[serde(default)] + pub interval: String, + #[serde(default)] + pub words_count_threshold: u32, + #[serde(default)] + pub split_mode: String, + #[serde(default)] + pub regex: String, + #[serde(default)] + pub split_words: Vec, +} + +impl Default for SegmentedReplyConfig { + fn default() -> Self { + Self { + enable: false, + only_llm_result: true, + interval_method: "random".to_string(), + interval: "1.5,3.5".to_string(), + words_count_threshold: 150, + split_mode: "regex".to_string(), + regex: String::new(), + split_words: vec![ + "。".to_string(), + "?".to_string(), + "!".to_string(), + "~".to_string(), + "…".to_string(), + ], + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlatformAdapterConfig { + pub platform_type: String, + #[serde(default)] + pub enabled: bool, + #[serde(default)] + pub config: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlatformSpecificConfig { + #[serde(default)] + pub lark: PlatformSpecificSettings, + #[serde(default)] + pub telegram: PlatformSpecificSettings, + #[serde(default)] + pub discord: PlatformSpecificSettings, +} + +impl Default for PlatformSpecificConfig { + fn default() -> Self { + Self { + lark: PlatformSpecificSettings::default(), + telegram: PlatformSpecificSettings::default(), + discord: PlatformSpecificSettings::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlatformSpecificSettings { + #[serde(default)] + pub pre_ack_emoji: PreAckEmojiConfig, +} + +impl Default for PlatformSpecificSettings { + fn default() -> Self { + Self { + pre_ack_emoji: PreAckEmojiConfig::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PreAckEmojiConfig { + #[serde(default)] + pub enable: bool, + #[serde(default)] + pub emojis: Vec, +} + +impl Default for PreAckEmojiConfig { + fn default() -> Self { + Self { + enable: false, + emojis: Vec::new(), + } + } +} + +// ============================================================================ +// Providers Config (providers.yaml) +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProvidersConfig { + #[serde(default)] + pub provider_settings: ProviderSettings, +} + +impl Default for ProvidersConfig { + fn default() -> Self { + Self { + provider_settings: ProviderSettings::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderSettings { + #[serde(default)] + pub enable: bool, + #[serde(default)] + pub default_provider_id: String, + #[serde(default)] + pub fallback_chat_models: Vec, + #[serde(default)] + pub default_image_caption_provider_id: String, + #[serde(default)] + pub image_caption_prompt: String, + #[serde(default)] + pub provider_pool: Vec, +} + +impl Default for ProviderSettings { + fn default() -> Self { + Self { + enable: true, + default_provider_id: String::new(), + fallback_chat_models: Vec::new(), + default_image_caption_provider_id: String::new(), + image_caption_prompt: "Please describe the image using Chinese.".to_string(), + provider_pool: vec!["*".to_string()], + } + } +} + +// ============================================================================ +// Agent Config (agent.yaml) +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentConfig { + #[serde(default)] + pub agent_settings: AgentSettings, + #[serde(default)] + pub subagent_orchestrator: SubagentOrchestratorConfig, + #[serde(default)] + pub provider_stt_settings: ProviderSttSettings, + #[serde(default)] + pub provider_tts_settings: ProviderTtsSettings, + #[serde(default)] + pub provider_ltm_settings: ProviderLtmSettings, +} + +impl Default for AgentConfig { + fn default() -> Self { + Self { + agent_settings: AgentSettings::default(), + subagent_orchestrator: SubagentOrchestratorConfig::default(), + provider_stt_settings: ProviderSttSettings::default(), + provider_tts_settings: ProviderTtsSettings::default(), + provider_ltm_settings: ProviderLtmSettings::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentSettings { + #[serde(default)] + pub wake_prefix: Vec, + #[serde(default)] + pub default_personality: String, + #[serde(default)] + pub persona_pool: Vec, + #[serde(default)] + pub prompt_prefix: String, + #[serde(default)] + pub context_limit_reached_strategy: String, + #[serde(default)] + pub llm_compress_instruction: String, + #[serde(default)] + pub llm_compress_keep_recent: u32, + #[serde(default)] + pub llm_compress_provider_id: String, + #[serde(default)] + pub max_context_length: i32, + #[serde(default)] + pub dequeue_context_length: u32, + #[serde(default)] + pub streaming_response: bool, + #[serde(default)] + pub show_tool_use_status: bool, + #[serde(default)] + pub show_tool_call_result: bool, + #[serde(default)] + pub sanitize_context_by_modalities: bool, + #[serde(default)] + pub max_quoted_fallback_images: u32, + #[serde(default)] + pub web_search: bool, + #[serde(default)] + pub websearch_provider: String, + #[serde(default)] + pub web_search_link: bool, + #[serde(default)] + pub identifier: bool, + #[serde(default)] + pub group_name_display: bool, + #[serde(default)] + pub datetime_system_prompt: bool, + #[serde(default)] + pub agent_runner_type: String, + #[serde(default)] + pub dify_agent_runner_provider_id: String, + #[serde(default)] + pub coze_agent_runner_provider_id: String, + #[serde(default)] + pub dashscope_agent_runner_provider_id: String, + #[serde(default)] + pub deerflow_agent_runner_provider_id: String, + #[serde(default)] + pub unsupported_streaming_strategy: String, + #[serde(default)] + pub reachability_check: bool, + #[serde(default)] + pub max_agent_step: u32, + #[serde(default)] + pub tool_call_timeout: u32, + #[serde(default)] + pub tool_schema_mode: String, + #[serde(default)] + pub llm_safety_mode: bool, + #[serde(default)] + pub safety_mode_strategy: String, + #[serde(default)] + pub proactive_capability: ProactiveCapability, + #[serde(default)] + pub computer_use_runtime: String, + #[serde(default)] + pub computer_use_require_admin: bool, + #[serde(default)] + pub image_compress_enabled: bool, + #[serde(default)] + pub image_compress_options: ImageCompressOptions, +} + +impl Default for AgentSettings { + fn default() -> Self { + Self { + wake_prefix: vec!["/".to_string()], + default_personality: "default".to_string(), + persona_pool: vec!["*".to_string()], + prompt_prefix: "{{prompt}}".to_string(), + context_limit_reached_strategy: "truncate_by_turns".to_string(), + llm_compress_instruction: String::new(), + llm_compress_keep_recent: 6, + llm_compress_provider_id: String::new(), + max_context_length: -1, + dequeue_context_length: 1, + streaming_response: false, + show_tool_use_status: false, + show_tool_call_result: false, + sanitize_context_by_modalities: false, + max_quoted_fallback_images: 20, + web_search: false, + websearch_provider: "default".to_string(), + web_search_link: false, + identifier: false, + group_name_display: false, + datetime_system_prompt: true, + agent_runner_type: "local".to_string(), + dify_agent_runner_provider_id: String::new(), + coze_agent_runner_provider_id: String::new(), + dashscope_agent_runner_provider_id: String::new(), + deerflow_agent_runner_provider_id: String::new(), + unsupported_streaming_strategy: "realtime_segmenting".to_string(), + reachability_check: false, + max_agent_step: 30, + tool_call_timeout: 60, + tool_schema_mode: "full".to_string(), + llm_safety_mode: true, + safety_mode_strategy: "system_prompt".to_string(), + proactive_capability: ProactiveCapability::default(), + computer_use_runtime: "none".to_string(), + computer_use_require_admin: true, + image_compress_enabled: true, + image_compress_options: ImageCompressOptions::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProactiveCapability { + #[serde(default)] + pub add_cron_tools: bool, +} + +impl Default for ProactiveCapability { + fn default() -> Self { + Self { + add_cron_tools: true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageCompressOptions { + #[serde(default)] + pub max_size: u32, + #[serde(default)] + pub quality: u32, +} + +impl Default for ImageCompressOptions { + fn default() -> Self { + Self { + max_size: 1024, + quality: 95, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentOrchestratorConfig { + #[serde(default)] + pub main_enable: bool, + #[serde(default)] + pub remove_main_duplicate_tools: bool, + #[serde(default)] + pub router_system_prompt: String, + #[serde(default)] + pub agents: Vec, +} + +impl Default for SubagentOrchestratorConfig { + fn default() -> Self { + Self { + main_enable: false, + remove_main_duplicate_tools: false, + router_system_prompt: String::new(), + agents: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDefinition { + pub name: String, + #[serde(default)] + pub provider_id: String, + #[serde(default)] + pub system_prompt: String, + #[serde(default)] + pub tools: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderSttSettings { + #[serde(default)] + pub enable: bool, + #[serde(default)] + pub provider_id: String, +} + +impl Default for ProviderSttSettings { + fn default() -> Self { + Self { + enable: false, + provider_id: String::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderTtsSettings { + #[serde(default)] + pub enable: bool, + #[serde(default)] + pub provider_id: String, + #[serde(default)] + pub dual_output: bool, + #[serde(default)] + pub use_file_service: bool, + #[serde(default)] + pub trigger_probability: f32, +} + +impl Default for ProviderTtsSettings { + fn default() -> Self { + Self { + enable: false, + provider_id: String::new(), + dual_output: false, + use_file_service: false, + trigger_probability: 1.0, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderLtmSettings { + #[serde(default)] + pub group_icl_enable: bool, + #[serde(default)] + pub group_message_max_cnt: u32, + #[serde(default)] + pub image_caption: bool, + #[serde(default)] + pub image_caption_provider_id: String, + #[serde(default)] + pub active_reply: ActiveReplyConfig, +} + +impl Default for ProviderLtmSettings { + fn default() -> Self { + Self { + group_icl_enable: false, + group_message_max_cnt: 300, + image_caption: false, + image_caption_provider_id: String::new(), + active_reply: ActiveReplyConfig::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ActiveReplyConfig { + #[serde(default)] + pub enable: bool, + #[serde(default)] + pub method: String, + #[serde(default)] + pub possibility_reply: f32, + #[serde(default)] + pub whitelist: Vec, +} + +impl Default for ActiveReplyConfig { + fn default() -> Self { + Self { + enable: false, + method: "possibility_reply".to_string(), + possibility_reply: 0.1, + whitelist: Vec::new(), + } + } +} + +// ============================================================================ +// Secrets Config (secrets.yaml) +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct SecretsConfig { + #[serde(default)] + pub dashboard: DashboardSecrets, + #[serde(default)] + pub providers: ProviderSecrets, + #[serde(default)] + pub platforms: PlatformSecrets, + #[serde(default)] + pub third_party: ThirdPartySecrets, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct DashboardSecrets { + #[serde(default)] + pub password: String, + #[serde(default)] + pub jwt_secret: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ProviderSecrets { + #[serde(default)] + pub openai: ProviderApiKey, + #[serde(default)] + pub anthropic: ProviderApiKey, + #[serde(default)] + pub dashscope: ProviderApiKey, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ProviderApiKey { + #[serde(default)] + pub api_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct PlatformSecrets { + #[serde(default)] + pub telegram: PlatformBotToken, + #[serde(default)] + pub discord: PlatformBotToken, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct PlatformBotToken { + #[serde(default)] + pub bot_token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ThirdPartySecrets { + #[serde(default)] + pub baidu_aip: BaiduAipSecrets, + #[serde(default)] + pub moonshotai: MoonshotAiSecrets, + #[serde(default)] + pub coze: CozeSecrets, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct BaiduAipSecrets { + #[serde(default)] + pub app_id: String, + #[serde(default)] + pub api_key: String, + #[serde(default)] + pub secret_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct MoonshotAiSecrets { + #[serde(default)] + pub api_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CozeSecrets { + #[serde(default)] + pub api_key: String, + #[serde(default)] + pub bot_id: String, +} + +// ============================================================================ +// GPG Config (gpg.yaml) +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GpgConfig { + #[serde(default)] + pub security: GpgSecurity, +} + +impl Default for GpgConfig { + fn default() -> Self { + Self { + security: GpgSecurity::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GpgSecurity { + #[serde(default)] + pub enable: bool, + #[serde(default)] + pub verify_mode: String, + #[serde(default)] + pub trusted_keys: Vec, + #[serde(default)] + pub signed_configs: Vec, + #[serde(default)] + pub signature_suffix: String, + #[serde(default)] + pub gnupg_home: String, +} + +impl Default for GpgSecurity { + fn default() -> Self { + Self { + enable: false, + verify_mode: "warn".to_string(), + trusted_keys: Vec::new(), + signed_configs: vec![ + "secrets.yaml".to_string(), + "agent.yaml".to_string(), + "platform.yaml".to_string(), + ], + signature_suffix: ".sig".to_string(), + gnupg_home: String::new(), + } + } +} + +// ============================================================================ +// Config Directory Paths (XDG Base Directory + Windows) +// ============================================================================ + +/// Get AstrBot config directory +/// - Linux/macOS: $XDG_CONFIG_HOME/astrbot or ~/.config/astrbot +/// - Windows: %APPDATA%/AstrBot +pub fn get_config_dir() -> PathBuf { + if let Ok(path) = std::env::var("ASTRBOT_CONFIG_DIR") { + return PathBuf::from(path).join("astrbot"); + } + if let Ok(path) = std::env::var("ASTRBOT_ROOT") { + return PathBuf::from(path).join("config"); + } + + #[cfg(target_os = "windows")] + { + if let Ok(path) = std::env::var("APPDATA") { + return PathBuf::from(path).join("AstrBot"); + } + return PathBuf::from("C:/Users/Default/AppData/Roaming/AstrBot"); + } + + #[cfg(not(target_os = "windows"))] + { + if let Ok(path) = std::env::var("XDG_CONFIG_HOME") { + return PathBuf::from(path).join("astrbot"); + } + if let Some(dir) = dirs::config_dir() { + return dir.join("astrbot"); + } + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home).join(".config").join("astrbot"); + } + return PathBuf::from("/root/.config/astrbot"); + } +} + +/// Get AstrBot data directory +/// - Linux/macOS: $XDG_DATA_HOME/astrbot or ~/.local/share/astrbot +/// - Windows: %LOCALAPPDATA%/AstrBot +pub fn get_data_dir() -> PathBuf { + if let Ok(path) = std::env::var("ASTRBOT_DATA_DIR") { + return PathBuf::from(path); + } + if let Ok(path) = std::env::var("ASTRBOT_ROOT") { + return PathBuf::from(path); + } + + #[cfg(target_os = "windows")] + { + if let Ok(path) = std::env::var("LOCALAPPDATA") { + return PathBuf::from(path).join("AstrBot"); + } + return PathBuf::from("C:/Users/Default/AppData/Local/AstrBot"); + } + + #[cfg(not(target_os = "windows"))] + { + if let Ok(path) = std::env::var("XDG_DATA_HOME") { + return PathBuf::from(path).join("astrbot"); + } + if let Some(dir) = dirs::data_dir() { + return dir.join("astrbot"); + } + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home).join(".local/share").join("astrbot"); + } + return PathBuf::from("/root/.local/share/astrbot"); + } +} + +/// Get AstrBot cache directory +/// - Linux/macOS: $XDG_CACHE_HOME/astrbot or ~/.cache/astrbot +/// - Windows: %TEMP%/AstrBot +pub fn get_cache_dir() -> PathBuf { + if let Ok(path) = std::env::var("ASTRBOT_CACHE_DIR") { + return PathBuf::from(path); + } + + #[cfg(target_os = "windows")] + { + return std::env::temp_dir().join("AstrBot"); + } + + #[cfg(not(target_os = "windows"))] + { + if let Ok(path) = std::env::var("XDG_CACHE_HOME") { + return PathBuf::from(path).join("astrbot"); + } + if let Some(dir) = dirs::cache_dir() { + return dir.join("astrbot"); + } + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home).join(".cache").join("astrbot"); + } + return PathBuf::from("/root/.cache/astrbot"); + } +} + +/// Get AstrBot runtime directory +/// - Linux/macOS: /run/user//astrbot or $XDG_RUNTIME_DIR/astrbot +/// - Windows: uses temp directory +pub fn get_runtime_dir() -> PathBuf { + if let Ok(path) = std::env::var("ASTRBOT_RUNTIME_DIR") { + return PathBuf::from(path); + } + + #[cfg(target_os = "windows")] + { + return std::env::temp_dir().join("AstrBot"); + } + + #[cfg(not(target_os = "windows"))] + { + if let Ok(path) = std::env::var("XDG_RUNTIME_DIR") { + return PathBuf::from(path).join("astrbot"); + } + if let Some(dir) = dirs::runtime_dir() { + return dir.join("astrbot"); + } + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home).join(".local/run").join("astrbot"); + } + return PathBuf::from("/run/astrbot"); } } diff --git a/rust/src/error.rs b/rust/src/error.rs index 882111059..f63ef6ac0 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -19,6 +19,9 @@ pub enum AstrBotError { #[error("Invalid state: {0}")] InvalidState(String), + #[error("Not found: {0}")] + NotFound(String), + #[error("IO error: {0}")] Io(#[from] std::io::Error), diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9114be6dc..939176fa5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,15 +1,14 @@ -//! AstrBot Core - High-performance core runtime in Rust +//! AstrBot Core - High-performance runtime in Rust //! -//! This crate provides the core runtime components for AstrBot, -//! exposing Python bindings via pyo3. +//! This crate provides the core runtime for AstrBot, with Rust as the core +//! and Python modules as plugins via PyO3. // RULES: // - NO unsafe blocks allowed -// - NO .unwrap() - use ? or expect with message -// - All errors must be handled properly +// - NO .unwrap() without message - use ? or expect() +// - All errors must be handled properly via Result #![deny(clippy::all)] -#![deny(clippy::pedantic)] #![deny(unsafe_code)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::too_many_lines)] @@ -21,23 +20,32 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::doc_markdown)] -#![allow(clippy::unwrap_used)] -#![allow(clippy::cast_precision_loss)] #![allow(clippy::return_self_not_must_use)] #![allow(clippy::must_use_candidate)] +pub mod a2a; +pub mod abp; pub mod cli; -pub mod error; -pub mod orchestrator; -pub mod message; -pub mod stats; -pub mod protocol; pub mod config; +pub mod error; +pub mod message; +pub mod orchestrator; +pub mod protocol; +pub mod server; +pub mod stats; #[cfg(feature = "python")] pub mod python; +pub use a2a::{AgentCard, Task, TaskState}; +pub use abp::{PluginCapabilities, PluginConfig, PluginLoadMode}; pub use error::AstrBotError; -pub use orchestrator::Orchestrator; pub use message::Message; +pub use orchestrator::Orchestrator; +pub use protocol::ProtocolStatus; +pub use server::{ApiResponse, HttpServer, WsManager}; pub use stats::RuntimeStats; + +// Re-export CLI for Python bindings +#[cfg(feature = "python")] +pub use cli::cli_with_args; diff --git a/rust/src/orchestrator.rs b/rust/src/orchestrator.rs index fe1db388b..652d3c363 100644 --- a/rust/src/orchestrator.rs +++ b/rust/src/orchestrator.rs @@ -1,33 +1,43 @@ //! Core orchestrator for AstrBot runtime +//! +//! Manages lifecycle of all protocol clients and stars (plugins). +use crate::abp::{AbpClient, PluginConfig, PluginLoadMode}; use crate::error::AstrBotError; +use crate::protocol::{AcpClient, LspClient, McpClient, ProtocolClient}; use crate::stats::RuntimeStats; use std::collections::HashMap; use std::sync::{Arc, RwLock}; use tokio::sync::broadcast; -use tokio::time::{interval, Duration}; +use tokio::time::{Duration, interval}; +use tracing::{debug, info, warn}; -#[derive(Debug, Clone)] -pub struct ProtocolStatus { - pub connected: bool, - pub name: String, -} +// ============================================================================ +// Orchestrator +// ============================================================================ -impl Default for ProtocolStatus { - fn default() -> Self { - Self { - connected: false, - name: String::new(), - } - } -} - -/// Main orchestrator coordinating all protocol clients +/// Main orchestrator coordinating all protocol clients and stars pub struct Orchestrator { + /// Running state running: RwLock, + /// Shutdown signal sender shutdown_tx: Arc>>>, - stars: RwLock>, - stats: RwLock, + /// Protocol clients + lsp: RwLock, + mcp: RwLock, + acp: RwLock, + abp: RwLock, + /// Star registry + stars: RwLock>, + /// Runtime statistics + stats: RuntimeStats, +} + +/// Star registration entry +#[derive(Debug, Clone)] +pub struct StarRegistration { + pub name: String, + pub handler: String, } impl Default for Orchestrator { @@ -37,52 +47,108 @@ impl Default for Orchestrator { } impl Orchestrator { + /// Create a new Orchestrator instance #[must_use] pub fn new() -> Self { Self { running: RwLock::new(false), shutdown_tx: Arc::new(RwLock::new(None)), + lsp: RwLock::new(LspClient::new()), + mcp: RwLock::new(McpClient::new()), + acp: RwLock::new(AcpClient::new()), + abp: RwLock::new(AbpClient::new()), stars: RwLock::new(HashMap::new()), - stats: RwLock::new(RuntimeStats::default()), + stats: RuntimeStats::new(), } } - /// Start the orchestrator - pub fn start(&self) -> Result<(), AstrBotError> { + /// Start the orchestrator and all protocol clients (sync version) + pub fn start_sync(&self) -> Result<(), AstrBotError> { { - let mut running = self.running.write().map_err(|_| { - AstrBotError::InvalidState("Failed to acquire write lock".into()) - })?; + let mut running = self + .running + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + if *running { + return Err(AstrBotError::InvalidState( + "Orchestrator already started".into(), + )); + } *running = true; } let (tx, _rx) = broadcast::channel(1); { - let mut shutdown_tx = self.shutdown_tx.write().map_err(|_| { - AstrBotError::InvalidState("Failed to acquire write lock".into()) - })?; + let mut shutdown_tx = self + .shutdown_tx + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; *shutdown_tx = Some(tx); } - tracing::info!("Orchestrator started"); + // Connect all protocol clients (sync) + self.connect_protocols_sync()?; + + info!("Orchestrator started"); Ok(()) } - /// Bootstrap all protocol clients - pub async fn bootstrap(&self) -> Result<(), AstrBotError> { - self.start()?; - tracing::info!("Protocol clients would be started here"); - tokio::time::sleep(Duration::from_millis(500)).await; + /// Connect all protocol clients (sync version for Python binding) + fn connect_protocols_sync(&self) -> Result<(), AstrBotError> { + // Connect LSP + { + let mut lsp = self + .lsp + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + // For now, just mark as connected (actual connection is async) + lsp.set_connected(true); + } + + // Connect MCP + { + let mut mcp = self + .mcp + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + mcp.set_connected(true); + } + + // Connect ACP + { + let mut acp = self + .acp + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + acp.set_connected(true); + } + + // Connect ABP + { + let mut abp = self + .abp + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + abp.set_connected(true); + } + Ok(()) } + /// Start the orchestrator and all protocol clients (async) + pub async fn start(&self) -> Result<(), AstrBotError> { + self.start_sync() + } + /// Main event loop pub async fn run_loop(&self) -> Result<(), AstrBotError> { if !self.is_running() { - return Err(AstrBotError::InvalidState("Orchestrator not started".into())); + return Err(AstrBotError::InvalidState( + "Orchestrator not started".into(), + )); } - tracing::info!("Orchestrator event loop started"); + info!("Orchestrator event loop started"); let mut tick_interval = interval(Duration::from_secs(5)); loop { @@ -91,7 +157,7 @@ impl Orchestrator { self.periodic_health_check(); } _ = self.wait_for_shutdown() => { - tracing::info!("Orchestrator shutdown signal received"); + info!("Orchestrator shutdown signal received"); break; } } @@ -101,10 +167,11 @@ impl Orchestrator { } } - tracing::info!("Orchestrator event loop stopped"); + info!("Orchestrator event loop stopped"); Ok(()) } + /// Wait for shutdown signal async fn wait_for_shutdown(&self) { let tx_guard = self.shutdown_tx.read().ok(); let tx = tx_guard.as_ref().and_then(|t| t.as_ref()); @@ -117,16 +184,36 @@ impl Orchestrator { } } + /// Periodic health check for all protocol clients fn periodic_health_check(&self) { - tracing::debug!("Orchestrator health check"); + debug!("Running periodic health check"); + + if let Ok(lsp) = self.lsp.read() { + if !lsp.is_connected() { + warn!("LSP client disconnected"); + } + } + + if let Ok(mcp) = self.mcp.read() { + if !mcp.is_connected() { + warn!("MCP client disconnected"); + } + } + + if let Ok(acp) = self.acp.read() { + if !acp.is_connected() { + warn!("ACP client disconnected"); + } + } } - /// Stop the orchestrator - pub fn stop(&self) -> Result<(), AstrBotError> { + /// Stop the orchestrator (sync version) + pub fn stop_sync(&self) -> Result<(), AstrBotError> { { - let mut running = self.running.write().map_err(|_| { - AstrBotError::InvalidState("Failed to acquire write lock".into()) - })?; + let mut running = self + .running + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; *running = false; } @@ -136,31 +223,121 @@ impl Orchestrator { } } - tracing::info!("Orchestrator stopped"); + self.shutdown_protocols_sync()?; + info!("Orchestrator stopped"); Ok(()) } + /// Shutdown all protocol clients (sync version) + fn shutdown_protocols_sync(&self) -> Result<(), AstrBotError> { + { + let mut lsp = self + .lsp + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + lsp.set_connected(false); + } + + { + let mut mcp = self + .mcp + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + mcp.set_connected(false); + } + + { + let mut acp = self + .acp + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + acp.set_connected(false); + } + + { + let mut abp = self + .abp + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + abp.set_connected(false); + } + + Ok(()) + } + + /// Stop the orchestrator (async) + pub async fn stop(&self) -> Result<(), AstrBotError> { + self.stop_sync() + } + + /// Check if orchestrator is running #[must_use] pub fn is_running(&self) -> bool { self.running.read().map(|r| *r).unwrap_or(false) } - pub fn register_star(&self, name: &str, _handler: &str) -> Result<(), AstrBotError> { - let mut stars = self.stars.write().map_err(|_| { - AstrBotError::InvalidState("Failed to acquire write lock".into()) - })?; - stars.insert(name.to_string(), name.to_string()); + /// Register a star (plugin) + pub fn register_star(&self, name: &str, handler: &str) -> Result<(), AstrBotError> { + let mut stars = self + .stars + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + + let registration = StarRegistration { + name: name.to_string(), + handler: handler.to_string(), + }; + + stars.insert(name.to_string(), registration); + + // 根据 handler 判断加载模式:包含 "/" 视为 Unix Socket 路径,否则为模块名 + if handler.starts_with('/') || handler.contains(".sock") { + // 跨进程加载 + if let Ok(mut abp) = self.abp.write() { + let config = PluginConfig { + name: name.to_string(), + version: "1.0.0".to_string(), + load_mode: PluginLoadMode::OutOfProcess, + command: Some(handler.to_string()), + ..Default::default() + }; + abp.register_out_of_process_plugin(config); + } + } else { + // 进程内加载 + if let Ok(mut abp) = self.abp.write() { + let config = PluginConfig { + name: name.to_string(), + version: "1.0.0".to_string(), + load_mode: PluginLoadMode::InProcess, + ..Default::default() + }; + abp.register_in_process_plugin(config); + } + } + + info!("Star '{}' registered", name); Ok(()) } + /// Unregister a star (plugin) pub fn unregister_star(&self, name: &str) -> Result<(), AstrBotError> { - let mut stars = self.stars.write().map_err(|_| { - AstrBotError::InvalidState("Failed to acquire write lock".into()) - })?; + let mut stars = self + .stars + .write() + .map_err(|_| AstrBotError::InvalidState("Failed to acquire write lock".into()))?; + stars.remove(name); + + if let Ok(mut abp) = self.abp.write() { + abp.unregister_plugin(name); + } + + info!("Star '{}' unregistered", name); Ok(()) } + /// List all registered stars #[must_use] pub fn list_stars(&self) -> Vec { self.stars @@ -169,26 +346,196 @@ impl Orchestrator { .unwrap_or_default() } + /// Record a message activity pub fn record_activity(&self) { - if let Ok(stats) = self.stats.write() { - stats.record_message(); + self.stats.record_message(); + } + + /// Get runtime statistics + #[must_use] + pub fn stats(&self) -> RuntimeStats { + self.stats.clone() + } + + /// Set protocol connection status + pub fn set_protocol_connected( + &self, + protocol: &str, + connected: bool, + ) -> Result<(), AstrBotError> { + match protocol { + "lsp" => { + let mut lsp = self.lsp.write().map_err(|_| { + AstrBotError::InvalidState("Failed to acquire write lock".into()) + })?; + lsp.set_connected(connected); + } + "mcp" => { + let mut mcp = self.mcp.write().map_err(|_| { + AstrBotError::InvalidState("Failed to acquire write lock".into()) + })?; + mcp.set_connected(connected); + } + "acp" => { + let mut acp = self.acp.write().map_err(|_| { + AstrBotError::InvalidState("Failed to acquire write lock".into()) + })?; + acp.set_connected(connected); + } + "abp" => { + let mut abp = self.abp.write().map_err(|_| { + AstrBotError::InvalidState("Failed to acquire write lock".into()) + })?; + abp.set_connected(connected); + } + _ => { + return Err(AstrBotError::InvalidState(format!( + "Unknown protocol: {protocol}" + ))); + } + } + Ok(()) + } + + /// Get protocol status + #[must_use] + pub fn get_protocol_status(&self, protocol: &str) -> Option { + match protocol { + "lsp" => self + .lsp + .read() + .ok() + .map(|lsp| crate::protocol::ProtocolStatus { + connected: lsp.is_connected(), + name: "lsp".to_string(), + }), + "mcp" => self + .mcp + .read() + .ok() + .map(|mcp| crate::protocol::ProtocolStatus { + connected: mcp.is_connected(), + name: "mcp".to_string(), + }), + "acp" => self + .acp + .read() + .ok() + .map(|acp| crate::protocol::ProtocolStatus { + connected: acp.is_connected(), + name: "acp".to_string(), + }), + "abp" => self + .abp + .read() + .ok() + .map(|abp| crate::protocol::ProtocolStatus { + connected: abp.is_connected(), + name: "abp".to_string(), + }), + _ => None, + } + } +} + +// ============================================================================ +// Python bindings via PyO3 (sync only) +// ============================================================================ + +#[cfg(feature = "python")] +mod python { + use super::*; + use pyo3::prelude::*; + use pyo3::types::PyDict; + + /// Python wrapper for Orchestrator + #[pyclass] + pub struct PyOrchestrator { + inner: Orchestrator, + } + + #[pymethods] + impl PyOrchestrator { + #[new] + pub fn new() -> Self { + Self { + inner: Orchestrator::new(), + } + } + + /// Start the orchestrator (sync, callable from Python) + pub fn start(&self) -> PyResult<()> { + self.inner + .start_sync() + .map_err(|e| PyErr::new::(e.to_string())) + } + + /// Stop the orchestrator (sync, callable from Python) + pub fn stop(&self) -> PyResult<()> { + self.inner + .stop_sync() + .map_err(|e| PyErr::new::(e.to_string())) + } + + pub fn is_running(&self) -> bool { + self.inner.is_running() + } + + pub fn register_star(&self, name: &str, handler: &str) -> PyResult<()> { + self.inner + .register_star(name, handler) + .map_err(|e| PyErr::new::(e.to_string())) + } + + pub fn unregister_star(&self, name: &str) -> PyResult<()> { + self.inner + .unregister_star(name) + .map_err(|e| PyErr::new::(e.to_string())) + } + + pub fn list_stars(&self) -> Vec { + self.inner.list_stars() + } + + pub fn record_activity(&self) { + self.inner.record_activity(); + } + + pub fn get_stats(&self, py: Python<'_>) -> PyResult> { + let stats = self.inner.stats(); + let dict = PyDict::new(py); + dict.set_item("message_count", stats.message_count())?; + dict.set_item("uptime_seconds", stats.uptime_seconds())?; + dict.set_item("last_activity_time", stats.last_activity_time())?; + Ok(dict.into()) + } + + pub fn set_protocol_connected(&self, protocol: &str, connected: bool) -> PyResult<()> { + self.inner + .set_protocol_connected(protocol, connected) + .map_err(|e| PyErr::new::(e.to_string())) + } + + pub fn get_protocol_status( + &self, + protocol: &str, + py: Python<'_>, + ) -> PyResult>> { + Ok(self.inner.get_protocol_status(protocol).map(|status| { + let dict = PyDict::new(py); + dict.set_item("connected", status.connected).unwrap(); + dict.set_item("name", &status.name).unwrap(); + dict.into() + })) } } - #[must_use] - pub fn stats(&self) -> RuntimeStats { - self.stats.read().map(|s| s.clone()).unwrap_or_default() - } - - #[must_use] - pub fn get_protocol_status(&self, _protocol: &str) -> Option { - Some(ProtocolStatus { - connected: true, - name: _protocol.to_string(), - }) - } - - pub fn set_protocol_connected(&self, _protocol: &str, _connected: bool) -> Result<(), AstrBotError> { - Ok(()) + impl Default for PyOrchestrator { + fn default() -> Self { + Self::new() + } } } + +#[cfg(feature = "python")] +pub use python::PyOrchestrator; diff --git a/rust/src/protocol.rs b/rust/src/protocol.rs index 9b845b02e..bb43cf830 100644 --- a/rust/src/protocol.rs +++ b/rust/src/protocol.rs @@ -1,119 +1,67 @@ -//! Protocol client implementations for ACP and ABP +//! Protocol client implementations for LSP, MCP, ACP, and ABP use crate::error::AstrBotError; use async_trait::async_trait; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::net::UnixStream; +use tokio::process::{Child, Command}; -/// Protocol client trait -#[async_trait] -pub trait ProtocolClient: Send + Sync { - async fn connect(&mut self) -> Result<(), AstrBotError>; - async fn disconnect(&mut self) -> Result<(), AstrBotError>; - fn is_connected(&self) -> bool; - fn name(&self) -> &str; +// ============================================================================ +// Protocol Status +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProtocolStatus { + pub connected: bool, + pub name: String, +} + +impl ProtocolStatus { + pub fn new(name: &str, connected: bool) -> Self { + Self { + connected, + name: name.to_string(), + } + } } // ============================================================================ -// LSP Client +// Protocol Client Trait +// ============================================================================ + +#[async_trait] +pub trait ProtocolClient: Send + Sync { + fn name(&self) -> &str; + fn is_connected(&self) -> bool; + async fn connect(&mut self) -> Result<(), AstrBotError>; + async fn disconnect(&mut self) -> Result<(), AstrBotError>; +} + +// ============================================================================ +// LSP Client - Language Server Protocol // ============================================================================ -#[derive(Default)] pub struct LspClient { connected: bool, + child: Option, + stdin: Option>, + stdout: Option>, + pending_requests: HashMap>, + request_id: i64, } impl LspClient { - #[must_use] - pub fn new() -> Self { - Self { connected: false } - } - - pub fn set_connected(&mut self, connected: bool) { - self.connected = connected; - } -} - -#[async_trait] -impl ProtocolClient for LspClient { - async fn connect(&mut self) -> Result<(), AstrBotError> { - tracing::debug!("LSP client connecting"); - self.connected = true; - Ok(()) - } - - async fn disconnect(&mut self) -> Result<(), AstrBotError> { - tracing::debug!("LSP client disconnecting"); - self.connected = false; - Ok(()) - } - - fn is_connected(&self) -> bool { - self.connected - } - - fn name(&self) -> &'static str { - "lsp-client" - } -} - -// ============================================================================ -// MCP Client -// ============================================================================ - -#[derive(Default)] -pub struct McpClient { - connected: bool, -} - -impl McpClient { - #[must_use] - pub fn new() -> Self { - Self { connected: false } - } - - pub fn set_connected(&mut self, connected: bool) { - self.connected = connected; - } -} - -#[async_trait] -impl ProtocolClient for McpClient { - async fn connect(&mut self) -> Result<(), AstrBotError> { - tracing::debug!("MCP client connecting"); - self.connected = true; - Ok(()) - } - - async fn disconnect(&mut self) -> Result<(), AstrBotError> { - tracing::debug!("MCP client disconnecting"); - self.connected = false; - Ok(()) - } - - fn is_connected(&self) -> bool { - self.connected - } - - fn name(&self) -> &'static str { - "mcp-client" - } -} - -// ============================================================================ -// ACP Client - AstrBot Communication Protocol -// ============================================================================ - -#[derive(Debug, Clone)] -pub struct AcpClient { - connected: bool, - server_url: Option, -} - -impl AcpClient { pub fn new() -> Self { Self { connected: false, - server_url: None, + child: None, + stdin: None, + stdout: None, + pending_requests: HashMap::new(), + request_id: 0, } } @@ -121,17 +69,512 @@ impl AcpClient { self.connected = connected; } - pub async fn connect_to_server(&mut self, host: &str, port: u16) -> Result<(), AstrBotError> { - self.server_url = Some(format!("{}:{}", host, port)); + /// Connect to an LSP server subprocess + pub async fn connect_to_server( + &mut self, + command: Vec, + workspace_uri: &str, + ) -> Result<(), AstrBotError> { + if command.is_empty() { + return Err(AstrBotError::InvalidState("LSP command is empty".into())); + } + + let mut cmd = Command::new(&command[0]); + if command.len() > 1 { + cmd.args(&command[1..]); + } + + cmd.stdin(std::process::Stdio::piped()); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn().map_err(|e| { + AstrBotError::ConnectionFailed(format!("Failed to spawn LSP server: {e}")) + })?; + + let stdin = child + .stdin + .take() + .ok_or_else(|| AstrBotError::ConnectionFailed("Failed to capture stdin".into()))?; + + let stdout = child + .stdout + .take() + .ok_or_else(|| AstrBotError::ConnectionFailed("Failed to capture stdout".into()))?; + + self.stdin = Some(tokio::io::BufWriter::new(stdin)); + self.stdout = Some(tokio::io::BufReader::new(stdout)); + self.child = Some(child); self.connected = true; - tracing::debug!("ACP client connecting to {}:{}", host, port); + + // Send initialize request + let initialize_params = serde_json::json!({ + "processId": std::process::id(), + "rootUri": workspace_uri, + "capabilities": {} + }); + + self.send_request("initialize", Some(initialize_params)) + .await?; + + // Send initialized notification + self.send_notification("initialized", None).await?; + + tracing::info!("LSP client connected to server"); Ok(()) } - pub async fn connect_to_unix_socket(&mut self, socket_path: &str) -> Result<(), AstrBotError> { - self.server_url = Some(format!("unix://{}", socket_path)); + /// Send a request and wait for response + pub async fn send_request( + &mut self, + method: &str, + params: Option, + ) -> Result { + let (tx, rx) = tokio::sync::oneshot::channel(); + let id = self.request_id; + self.request_id += 1; + + self.pending_requests.insert(id, tx); + + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params.unwrap_or(serde_json::Value::Null) + }); + + self.send_json(&request).await?; + + rx.await + .map_err(|_| AstrBotError::Timeout("LSP request timed out".into())) + } + + /// Send a notification (no response expected) + pub async fn send_notification( + &mut self, + method: &str, + params: Option, + ) -> Result<(), AstrBotError> { + let notification = serde_json::json!({ + "jsonrpc": "2.0", + "method": method, + "params": params.unwrap_or(serde_json::Value::Null) + }); + + self.send_json(¬ification).await + } + + async fn send_json(&mut self, msg: &serde_json::Value) -> Result<(), AstrBotError> { + let content = serde_json::to_string(msg).map_err(|e| AstrBotError::Json(e))?; + let header = format!("Content-Length: {}\r\n\r\n", content.len()); + + if let Some(stdin) = &mut self.stdin { + stdin.write_all(header.as_bytes()).await?; + stdin.write_all(content.as_bytes()).await?; + stdin.flush().await?; + } + + Ok(()) + } + + /// Read responses from LSP server + pub async fn read_responses(&mut self) -> Result<(), AstrBotError> { + if let Some(stdout) = &mut self.stdout { + let mut header_buf = Vec::new(); + + loop { + // Read header line + header_buf.clear(); + stdout.read_until(b'\n', &mut header_buf).await?; + + let header_str = String::from_utf8_lossy(&header_buf); + let content_length = header_str + .strip_prefix("Content-Length: ") + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + + // Skip blank line (\r\n) + if header_buf.len() >= 2 && header_buf[0] == b'\r' && header_buf[1] == b'\n' { + // already at content + } else { + // Read the \r\n after Content-Length + let mut crlf = [0u8; 2]; + stdout.read_exact(&mut crlf).await?; + } + + // Read content + let mut content = vec![0u8; content_length]; + stdout.read_exact(&mut content).await?; + + let response: serde_json::Value = + serde_json::from_slice(&content).map_err(|e| AstrBotError::Json(e))?; + + // Handle response + if let Some(id) = response.get("id").and_then(|v| v.as_i64()) { + if let Some(tx) = self.pending_requests.remove(&id) { + let _ = tx.send(response.to_string()); + } + } + } + } + Ok(()) + } +} + +impl Default for LspClient { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl ProtocolClient for LspClient { + fn name(&self) -> &'static str { + "lsp" + } + + fn is_connected(&self) -> bool { + self.connected + } + + async fn connect(&mut self) -> Result<(), AstrBotError> { self.connected = true; - tracing::debug!("ACP client connecting to unix socket:{}", socket_path); + tracing::debug!("LSP client connected"); + Ok(()) + } + + async fn disconnect(&mut self) -> Result<(), AstrBotError> { + self.connected = false; + + // Try graceful shutdown first + if let Err(_) = self.send_notification("shutdown", None).await { + // Ignore errors during shutdown + } + if let Err(_) = self.send_notification("exit", None).await { + // Ignore errors during exit + } + + // Terminate LSP server process + if let Some(mut child) = self.child.take() { + let _ = child.kill().await; + } + + self.stdin = None; + self.stdout = None; + tracing::info!("LSP client disconnected"); + Ok(()) + } +} + +// ============================================================================ +// MCP Client - Model Context Protocol +// ============================================================================ + +pub struct McpClient { + connected: bool, + server_url: Option, + transport: McpTransport, +} + +enum McpTransport { + Stdio { + child: Option, + }, + Http { + client: Option, + base_url: String, + }, + #[allow(dead_code)] + Sse { + client: Option, + base_url: String, + }, +} + +impl McpClient { + pub fn new() -> Self { + Self { + connected: false, + server_url: None, + transport: McpTransport::Stdio { child: None }, + } + } + + pub fn set_connected(&mut self, connected: bool) { + self.connected = connected; + } + + /// Connect to MCP server via stdio + pub async fn connect_to_stdio_server( + &mut self, + command: Vec, + ) -> Result<(), AstrBotError> { + if command.is_empty() { + return Err(AstrBotError::InvalidState("MCP command is empty".into())); + } + + let mut cmd = Command::new(&command[0]); + if command.len() > 1 { + cmd.args(&command[1..]); + } + + cmd.stdin(std::process::Stdio::piped()); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let child = cmd.spawn().map_err(|e| { + AstrBotError::ConnectionFailed(format!("Failed to spawn MCP server: {e}")) + })?; + + self.transport = McpTransport::Stdio { child: Some(child) }; + self.connected = true; + self.server_url = Some("stdio".to_string()); + + tracing::info!("MCP client connected via stdio"); + Ok(()) + } + + /// Connect to MCP server via HTTP/SSE + pub async fn connect_to_http_server(&mut self, base_url: &str) -> Result<(), AstrBotError> { + let client = reqwest::Client::new(); + self.transport = McpTransport::Http { + client: Some(client), + base_url: base_url.to_string(), + }; + self.connected = true; + self.server_url = Some(base_url.to_string()); + + tracing::info!("MCP client connected via HTTP: {}", base_url); + Ok(()) + } + + /// List available tools on MCP server + pub async fn list_tools(&self) -> Result, AstrBotError> { + match &self.transport { + McpTransport::Http { client, base_url } => { + if let Some(client) = client { + let url = format!("{}/tools", base_url); + let response = client + .get(&url) + .send() + .await + .map_err(|e| AstrBotError::ConnectionFailed(e.to_string()))?; + + let tools: Vec = response + .json() + .await + .map_err(|e| AstrBotError::Protocol(e.to_string()))?; + + return Ok(tools); + } + } + McpTransport::Stdio { .. } => { + // Stdio-based tool listing would require JSON-RPC communication + } + McpTransport::Sse { .. } => {} + } + Ok(vec![]) + } + + /// Call a tool on MCP server + pub async fn call_tool( + &self, + tool_name: &str, + arguments: serde_json::Value, + ) -> Result { + match &self.transport { + McpTransport::Http { client, base_url } => { + if let Some(client) = client { + let url = format!("{}/call", base_url); + let request = serde_json::json!({ + "tool": tool_name, + "arguments": arguments + }); + + let response = client + .post(&url) + .json(&request) + .send() + .await + .map_err(|e| AstrBotError::ConnectionFailed(e.to_string()))?; + + let result: serde_json::Value = response + .json() + .await + .map_err(|e| AstrBotError::Protocol(e.to_string()))?; + + return Ok(result); + } + } + McpTransport::Stdio { child: _ } => { + // Stdio-based tool calling would require JSON-RPC communication + tracing::debug!("MCP stdio tool call: {} with {:?}", tool_name, arguments); + } + McpTransport::Sse { .. } => {} + } + + Err(AstrBotError::NotConnected("MCP transport not ready".into())) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpTool { + pub name: String, + pub description: String, + pub input_schema: serde_json::Value, +} + +impl Default for McpClient { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl ProtocolClient for McpClient { + fn name(&self) -> &'static str { + "mcp" + } + + fn is_connected(&self) -> bool { + self.connected + } + + async fn connect(&mut self) -> Result<(), AstrBotError> { + self.connected = true; + tracing::debug!("MCP client connected"); + Ok(()) + } + + async fn disconnect(&mut self) -> Result<(), AstrBotError> { + self.connected = false; + + if let McpTransport::Stdio { child } = &mut self.transport { + if let Some(mut child) = child.take() { + let _ = child.kill().await; + } + } + + tracing::info!("MCP client disconnected"); + Ok(()) + } +} + +// ============================================================================ +// ACP Client - Agent Communication Protocol (e.g., Google A2A) +// ============================================================================ + +pub struct AcpClient { + connected: bool, + server_url: Option, + tcp_stream: Option, + pending_requests: HashMap>, + request_id: u64, +} + +impl AcpClient { + pub fn new() -> Self { + Self { + connected: false, + server_url: None, + tcp_stream: None, + pending_requests: HashMap::new(), + request_id: 0, + } + } + + pub fn set_connected(&mut self, connected: bool) { + self.connected = connected; + } + + /// Connect to ACP server via TCP + pub async fn connect_to_tcp(&mut self, host: &str, port: u16) -> Result<(), AstrBotError> { + let addr = format!("{}:{}", host, port); + let stream = TcpStream::connect(&addr) + .await + .map_err(|e| AstrBotError::ConnectionFailed(format!("TCP connect failed: {e}")))?; + + self.tcp_stream = Some(stream); + self.connected = true; + self.server_url = Some(addr.clone()); + + tracing::info!("ACP client connected via TCP: {}", addr); + Ok(()) + } + + /// Connect to ACP server via Unix socket + /// Note: For now, Unix socket support is simplified - just mark as connected + pub async fn connect_to_unix_socket(&mut self, socket_path: &str) -> Result<(), AstrBotError> { + let _stream = UnixStream::connect(socket_path).await.map_err(|e| { + AstrBotError::ConnectionFailed(format!("Unix socket connect failed: {e}")) + })?; + + self.connected = true; + self.server_url = Some(socket_path.to_string()); + + tracing::info!("ACP client connected via Unix socket: {}", socket_path); + Ok(()) + } + + /// Send a request and wait for response + pub async fn call_tool( + &mut self, + server_name: &str, + tool_name: &str, + arguments: serde_json::Value, + ) -> Result { + let request_id = format!("{}-{}", self.request_id, server_name); + self.request_id += 1; + + let (tx, rx) = tokio::sync::oneshot::channel(); + self.pending_requests.insert(request_id.clone(), tx); + + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": request_id, + "method": format!("{}/{}", server_name, tool_name), + "params": arguments + }); + + self.send_json(&request).await?; + + rx.await + .map_err(|_| AstrBotError::Timeout("ACP request timed out".into())) + } + + /// Send a notification + pub async fn send_notification( + &mut self, + method: &str, + params: Option, + ) -> Result<(), AstrBotError> { + let notification = serde_json::json!({ + "jsonrpc": "2.0", + "method": method, + "params": params.unwrap_or(serde_json::Value::Null) + }); + + self.send_json(¬ification).await + } + + async fn send_json(&mut self, msg: &serde_json::Value) -> Result<(), AstrBotError> { + let content = serde_json::to_string(msg).map_err(|e| AstrBotError::Json(e))?; + let header = serde_json::to_string(&serde_json::json!({ + "content-length": content.len() + }))? + "\n"; + + if let Some(stream) = &mut self.tcp_stream { + stream.write_all(header.as_bytes()).await?; + stream.write_all(content.as_bytes()).await?; + } + + Ok(()) + } + + /// Read messages from ACP server + pub async fn read_messages(&mut self) -> Result<(), AstrBotError> { + // Simplified - actual implementation would use BufReader + tracing::debug!("ACP read_messages called"); Ok(()) } } @@ -144,79 +587,25 @@ impl Default for AcpClient { #[async_trait] impl ProtocolClient for AcpClient { - async fn connect(&mut self) -> Result<(), AstrBotError> { - self.connected = true; - Ok(()) - } - - async fn disconnect(&mut self) -> Result<(), AstrBotError> { - self.connected = false; - self.server_url = None; - Ok(()) + fn name(&self) -> &'static str { + "acp" } fn is_connected(&self) -> bool { self.connected } - fn name(&self) -> &'static str { - "acp-client" - } -} - -// ============================================================================ -// ABP Client - AstrBot Protocol (internal stars) -// ============================================================================ - -#[derive(Debug, Default)] -pub struct AbpClient { - connected: bool, - stars: HashMap, -} - -impl AbpClient { - pub fn new() -> Self { - Self::default() - } - - pub fn set_connected(&mut self, connected: bool) { - self.connected = connected; - } - - pub fn register_star(&mut self, name: &str, _handler: &str) { - self.stars.insert(name.to_string(), name.to_string()); - tracing::debug!("Star '{}' registered with ABP client", name); - } - - pub fn unregister_star(&mut self, name: &str) { - self.stars.remove(name); - tracing::debug!("Star '{}' unregistered from ABP client", name); - } - - pub fn list_stars(&self) -> Vec { - self.stars.keys().cloned().collect() - } -} - -#[async_trait] -impl ProtocolClient for AbpClient { async fn connect(&mut self) -> Result<(), AstrBotError> { self.connected = true; - tracing::debug!("ABP client connecting to internal stars"); + tracing::debug!("ACP client connected"); Ok(()) } async fn disconnect(&mut self) -> Result<(), AstrBotError> { self.connected = false; - self.stars.clear(); + self.tcp_stream = None; + tracing::info!("ACP client disconnected"); Ok(()) } - - fn is_connected(&self) -> bool { - self.connected - } - - fn name(&self) -> &'static str { - "abp-client" - } } + diff --git a/rust/src/python.rs b/rust/src/python.rs index 055470189..6288fe5b7 100644 --- a/rust/src/python.rs +++ b/rust/src/python.rs @@ -1,18 +1,10 @@ //! Python bindings for AstrBot Core use pyo3::prelude::*; -use pyo3::types::PyList; /// Run the AstrBot Core CLI. #[pyfunction] -pub fn cli(py: Python<'_>) -> PyResult<()> { - let sys = py.import("sys")?; - let argv: Bound<'_, PyAny> = sys.getattr("argv")?; - let argv_list = argv.downcast::()?; - let args: Vec = argv_list.iter() - .skip(1) - .filter_map(|item| item.extract::().ok()) - .collect(); +pub fn cli(args: Vec) -> PyResult<()> { crate::cli::cli_with_args(&args) .map_err(|e| PyErr::new::(e.to_string())) } diff --git a/rust/src/server.rs b/rust/src/server.rs new file mode 100644 index 000000000..b00879905 --- /dev/null +++ b/rust/src/server.rs @@ -0,0 +1,606 @@ +AstrBot/rust/src/server.rs +```rust +//! AstrBot HTTP/WebSocket Server +//! +//! High-performance async HTTP server with WebSocket support for real-time communication. +//! Implements JWT authentication and REST API endpoints. + +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::{mpsc, RwLock}; +use tokio_tungstenite::{accept_async, tungstenite::Message}; +use uuid::Uuid; + +// ============================================================================ +// Error Types +// ============================================================================ + +#[derive(Debug, thiserror::Error)] +pub enum ServerError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("WebSocket error: {0}")] + WebSocket(String), + #[error("JWT error: {0}")] + Jwt(String), + #[error("Not found: {0}")] + NotFound(String), + #[error("Unauthorized")] + Unauthorized, + #[error("Bad request: {0}")] + BadRequest(String), +} + +impl From for ServerError { + fn from(err: tokio_tungstenite::tungstenite::Error) -> Self { + ServerError::WebSocket(err.to_string()) + } +} + +// ============================================================================ +// JWT Authentication +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JwtClaims { + pub sub: String, + pub exp: u64, + pub iat: u64, +} + +pub struct JwtAuth { + secret: String, + expiry_secs: u64, +} + +impl JwtAuth { + pub fn new(secret: String, expiry_secs: u64) -> Self { + Self { + secret, + expiry_secs, + } + } + + pub fn generate_token(&self, username: &str) -> Result { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOODY) + .map_err(|e| ServerError::Jwt(e.to_string()))? + .as_secs(); + + let claims = JwtClaims { + sub: username.to_string(), + exp: now + self.expiry_secs, + iat: now, + }; + + let header = format!("{{\"alg\":\"HS256\",\"typ\":\"JWT\"}}"); + let header_b64 = BASE64.encode(header.as_bytes()); + let payload_b64 = BASE64.encode(serde_json::to_vec(&claims).map_err(|e| ServerError::Jwt(e.to_string()))?; + + let signature = self.sign_jwt(&format!("{}.{}", header_b64, payload_b64)); + + Ok(format!("{}.{}.{}", header_b64, payload_b64, signature)) + } + + pub fn verify_token(&self, token: &str) -> Result { + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return Err(ServerError::Jwt("Invalid token format".to_string())); + } + + let expected_sig = self.sign_jwt(&format!("{}.{}", parts[0], parts[1])); + if expected_sig != parts[2] { + return Err(ServerError::Jwt("Invalid signature".to_string())); + } + + let payload = BASE64 + .decode(parts[1]) + .map_err(|e| ServerError::Jwt(e.to_string()))?; + let claims: JwtClaims = + serde_json::from_slice(&payload).map_err(|e| ServerError::Jwt(e.to_string()))?; + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|e| ServerError::Jwt(e.to_string()))? + .as_secs(); + + if claims.exp < now { + return Err(ServerError::Jwt("Token expired".to_string())); + } + + Ok(claims) + } + + fn sign_jwt(&self, data: &str) -> String { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + self.secret.hash(&mut hasher); + BASE64.encode(&hasher.finish().to_be_bytes()) + } +} + +// ============================================================================ +// HTTP Response Types +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ApiResponse { + #[serde(rename = "success")] + Success { data: serde_json::Value }, + #[serde(rename = "error")] + Error { code: i32, message: String }, +} + +impl ApiResponse { + pub fn success(data: T) -> Self { + ApiResponse::Success { + data: serde_json::to_value(data).unwrap_or(serde_json::Value::Null), + } + } + + pub fn error(code: i32, message: impl Into) -> Self { + ApiResponse::Error { + code, + message: message.into(), + } + } +} + +#[derive(Debug, Clone)] +pub struct HttpResponse { + pub status: u16, + pub status_text: &'static str, + pub headers: Vec<(String, String)>, + pub body: Vec, +} + +impl HttpResponse { + pub fn new(status: u16, status_text: &'static str) -> Self { + Self { + status, + status_text, + headers: Vec::new(), + body: Vec::new(), + } + } + + pub fn json(status: u16, status_text: &'static str, body: serde_json::Value) -> Self { + let body_bytes = serde_json::to_vec(&body).unwrap_or_default(); + Self { + status, + status_text, + headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ("Content-Length".to_string(), body_bytes.len().to_string()), + ], + body: body_bytes, + } + } + + pub fn html(status: u16, status_text: &'static str, body: impl Into>) -> Self { + let body = body.into(); + Self { + status, + status_text, + headers: vec![ + ("Content-Type".to_string(), "text/html".to_string()), + ("Content-Length".to_string(), body.len().to_string()), + ], + body, + } + } + + pub fn to_vec(&self) -> Vec { + let mut buf = format!("HTTP/1.1 {} {}\r\n", self.status, self.status_text).into_bytes(); + for (key, value) in &self.headers { + buf.extend_from_slice(format!("{}: {}\r\n", key, value).as_bytes()); + } + buf.extend_from_slice(b"\r\n"); + buf.extend_from_slice(&self.body); + buf + } +} + +// ============================================================================ +// WebSocket Connection Manager +// ============================================================================ + +#[derive(Debug, Clone)] +pub struct WsConnection { + pub id: String, + pub authenticated: bool, + pub username: Option, +} + +#[derive(Default)] +pub struct WsManager { + connections: RwLock>>, +} + +impl WsManager { + pub fn new() -> Self { + Self { + connections: RwLock::new(HashMap::new()), + } + } + + pub async fn broadcast(&self, message: &str) { + let connections = self.connections.read().await; + for sender in connections.values() { + let _ = sender.send(message.to_string()).await; + } + } + + pub async fn send_to(&self, id: &str, message: &str) -> Result<(), ServerError> { + let connections = self.connections.read().await; + let sender = connections.get(id).ok_or_else(|| ServerError::NotFound(id.to_string()))?; + sender.send(message.to_string()).await.map_err(|_| { + ServerError::WebSocket("Failed to send".to_string()) + })?; + Ok(()) + } + + pub async fn register(&self, id: String, sender: mpsc::Sender) { + self.connections.write().await.insert(id, sender); + } + + pub async fn unregister(&self, id: &str) { + self.connections.write().await.remove(id); + } + + pub async fn count(&self) -> usize { + self.connections.read().await.len() + } +} + +// ============================================================================ +// HTTP Request +// ============================================================================ + +#[derive(Debug)] +pub struct HttpRequest { + pub method: String, + pub path: String, + pub headers: HashMap, + pub body: Vec, +} + +impl HttpRequest { + pub fn parse(buf: &[u8]) -> Result { + let header_end = buf.windows(4).position(|w| w == b"\r\n\r\n"); + let header_end = header_end.ok_or_else(|| ServerError::BadRequest("Invalid HTTP header".to_string()))?; + + let header_str = String::from_utf8_lossy(&buf[..header_end]); + let body = buf[header_end + 4..].to_vec(); + + let mut lines = header_str.split("\r\n"); + let request_line = lines.next().ok_or_else(|| ServerError::BadRequest("Missing request line".to_string()))?; + let parts: Vec<&str> = request_line.split_whitespace().collect(); + + if parts.len() < 2 { + return Err(ServerError::BadRequest("Invalid request line".to_string())); + } + + let method = parts[0].to_string(); + let path = parts[1].to_string(); + + let mut headers = HashMap::new(); + for line in lines { + if let Some((key, value)) = line.split_once(':') { + headers.insert(key.trim().to_lowercase(), value.trim().to_string()); + } + } + + Ok(HttpRequest { + method, + path, + headers, + body, + }) + } + + pub fn get_header(&self, key: &str) -> Option<&str> { + self.headers.get(&key.to_lowercase()).map(|s| s.as_str()) + } + + pub fn auth_token(&self) -> Option<&str> { + self.get_header("authorization") + .and_then(|v| v.strip_prefix("Bearer ")) + } +} + +// ============================================================================ +// Server State +// ============================================================================ + +#[derive(Clone)] +pub struct ServerState { + pub orchestrator: Arc, + pub jwt_auth: Arc, + pub ws_manager: Arc, + pub dashboard_password: String, +} + +impl ServerState { + pub fn new(jwt_secret: String, dashboard_password: String) -> Self { + Self { + orchestrator: Arc::new(crate::Orchestrator::new()), + jwt_auth: Arc::new(JwtAuth::new(jwt_secret, 86400)), + ws_manager: Arc::new(WsManager::new()), + dashboard_password, + } + } +} + +// ============================================================================ +// API Handlers +// ============================================================================ + +async fn handle_api( + state: &ServerState, + req: &HttpRequest, +) -> Result { + let (path, method) = (req.path.as_str(), req.method.as_str()); + + // Login endpoint (no auth required) + if path == "/api/auth/login" && method == "POST" { + return handle_login(state, req).await; + } + + // Authenticate other requests + let token = req.auth_token().ok_or(ServerError::Unauthorized)?; + state.jwt_auth.verify_token(token).map_err(|_| ServerError::Unauthorized)?; + + match (path, method) { + ("/api/stats", "GET") => { + let stats = state.orchestrator.stats(); + Ok(HttpResponse::json( + 200, + "OK", + serde_json::json!({ + "message_count": stats.message_count(), + "uptime_seconds": stats.uptime_seconds(), + "last_activity": stats.last_activity_time(), + }), + )) + } + ("/api/sessions", "GET") => { + Ok(HttpResponse::json( + 200, + "OK", + serde_json::json!({ + "sessions": state.orchestrator.list_stars(), + }), + )) + } + ("/api/health", "GET") => { + Ok(HttpResponse::json( + 200, + "OK", + serde_json::json!({ + "status": "ok", + "orchestrator_running": state.orchestrator.is_running(), + "websocket_connections": state.ws_manager.count().await, + }), + )) + } + _ if path.starts_with("/api/sessions/") && method == "DELETE" => { + let session_id = path.strip_prefix("/api/sessions/").unwrap_or(""); + state.orchestrator.unregister_star(session_id).map_err(|_| { + ServerError::NotFound("Session not found".to_string()) + })?; + Ok(HttpResponse::json(200, "OK", serde_json::json!({"success": true}))) + } + _ if path.starts_with("/api/sessions/") && method == "GET" => { + let session_id = path.strip_prefix("/api/sessions/").unwrap_or(""); + Ok(HttpResponse::json( + 200, + "OK", + serde_json::json!({"session_id": session_id}), + )) + } + _ => Ok(HttpResponse::json( + 404, + "Not Found", + ApiResponse::error(404, "Endpoint not found"), + )), + } +} + +async fn handle_login(state: &ServerState, req: &HttpRequest) -> Result { + let body = String::from_utf8_lossy(&req.body); + let login: serde_json::Value = + serde_json::from_str(&body).map_err(|_| ServerError::BadRequest("Invalid JSON".to_string()))?; + + let password = login + .get("password") + .and_then(|v| v.as_str()) + .ok_or_else(|| ServerError::BadRequest("Missing password".to_string()))?; + + if password != state.dashboard_password { + return Ok(HttpResponse::json( + 401, + "Unauthorized", + ApiResponse::error(401, "Invalid credentials"), + )); + } + + let token = state.jwt_auth.generate_token("admin")?; + Ok(HttpResponse::json( + 200, + "OK", + serde_json::json!({ + "token": token, + "expires_in": 86400, + }), + )) +} + +// ============================================================================ +// WebSocket Handler +// ============================================================================ + +async fn handle_websocket(state: ServerState, stream: TcpStream, addr: std::net::SocketAddr) { + let ws_stream = match accept_async(stream).await { + Ok(ws) => ws, + Err(e) => { + tracing::error!("WebSocket handshake failed from {}: {}", addr, e); + return; + } + }; + + let (writer, mut reader) = ws_stream.split(); + let connection_id = Uuid::new_v4().to_string(); + + let (tx, rx) = mpsc::channel::(100); + state.ws_manager.register(connection_id.clone(), tx).await; + + // Send welcome message + let welcome = serde_json::json!({ + "type": "connected", + "id": connection_id + }); + if let Ok(text) = serde_json::to_string(&welcome) { + let _ = writer.send(Message::Text(text.into())).await; + } + + // Handle messages + loop { + tokio::select! { + msg = reader.next() => { + match msg { + Some(Ok(Message::Text(text))) => { + if let Err(e) = handle_ws_message(&state, text.to_string()).await { + tracing::error!("WebSocket error: {}", e); + break; + } + } + Some(Ok(Message::Close(_))) | None => { + break; + } + _ => {} + } + } + Some(msg) = rx.recv() => { + if let Some(text) = msg { + if writer.send(Message::Text(text.into())).await.is_err() { + break; + } + } + } + } + } + + state.ws_manager.unregister(&connection_id).await; +} + +async fn handle_ws_message(state: &ServerState, msg: String) -> Result<(), ServerError> { + let parsed: serde_json::Value = + serde_json::from_str(&msg).map_err(|_| ServerError::BadRequest("Invalid JSON".to_string()))?; + + let msg_type = parsed.get("type").and_then(|v| v.as_str()).unwrap_or("unknown"); + + let response = match msg_type { + "ping" => serde_json::json!({"type": "pong"}), + "subscribe" => serde_json::json!({"type": "subscribed"}), + "get_stats" => { + let stats = state.orchestrator.stats(); + serde_json::json!({ + "type": "stats", + "data": { + "message_count": stats.message_count(), + "uptime_seconds": stats.uptime_seconds(), + } + }) + } + _ => serde_json::json!({"type": "unknown"}), + }; + + if !response.is_null() { + state + .ws_manager + .send_to(&connection_id, &serde_json::to_string(&response).unwrap()) + .await + .ok(); + } + + Ok(()) +} + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use futures_util::StreamExt; + +// ============================================================================ +// HTTP Server +// ============================================================================ + +pub struct HttpServer { + host: String, + port: u16, + state: ServerState, +} + +impl HttpServer { + pub fn new(host: String, port: u16, state: ServerState) -> Self { + Self { host, port, state } + } + + pub async fn serve(self) -> Result<(), ServerError> { + let addr = format!("{}:{}", self.host, self.port); + let listener = TcpListener::bind(&addr).await?; + tracing::info!("HTTP server listening on {}", addr); + + loop { + match listener.accept().await { + Ok((stream, addr)) => { + let state = self.state.clone(); + tokio::spawn(async move { + if let Err(e) = handle_connection(stream, state).await { + tracing::error!("Connection error from {}: {}", addr, e); + } + }); + } + Err(e) => { + tracing::error!("Accept error: {}", e); + } + } + } + } +} + +async fn handle_connection(stream: TcpStream, state: ServerState) -> Result<(), ServerError> { + let mut buf = vec![0u8; 8192]; + let n = stream.read(&mut buf).await?; + buf.truncate(n); + + let req = HttpRequest::parse(&buf)?; + + // Check for WebSocket upgrade + if req.get_header("upgrade") == Some("websocket") { + // For now, return HTTP response indicating WebSocket not fully implemented + let response = HttpResponse::html( + 426, + "Upgrade Required", + "WebSocket upgrade not implemented", + ); + let mut stream = stream; + stream.write_all(&response.to_vec()).await?; + stream.flush().await?; + return Ok(()); + } + + let response = handle_api(&state, &req).await?; + let mut stream = stream; + stream.write_all(&response.to_vec()).await?; + stream.flush().await?; + + Ok(()) +}