diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 256a73e..ac84dd8 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -134,6 +134,15 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -164,6 +173,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -218,57 +237,23 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.7.0", - "hyper-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper 1.0.2", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "axum" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.2", + "axum-core", "bytes", "form_urlencoded", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.7.0", + "hyper", "hyper-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -278,7 +263,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower", "tower-layer", @@ -286,27 +271,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.2", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "axum-core" version = "0.5.2" @@ -315,13 +279,13 @@ checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -383,12 +347,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.9.2" @@ -598,20 +556,21 @@ dependencies = [ [[package]] name = "config" -version = "0.14.1" +version = "0.15.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +checksum = "cef036f0ecf99baef11555578630e2cca559909b4c50822dbba828c252d21c49" dependencies = [ "async-trait", "convert_case", "json5", - "nom", "pathdiff", "ron", "rust-ini", - "serde", + "serde-untagged", + "serde_core", "serde_json", "toml", + "winnow", "yaml-rust2", ] @@ -652,9 +611,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -765,6 +724,24 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "der" version = "0.7.10" @@ -786,6 +763,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "digest" version = "0.10.7" @@ -854,6 +842,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -884,9 +883,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" -version = "0.4.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" @@ -895,6 +894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1066,9 +1066,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1085,16 +1087,16 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.3.27" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", - "http 0.2.12", + "http", "indexmap 2.10.0", "slab", "tokio", @@ -1116,10 +1118,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", - "allocator-api2", -] [[package]] name = "hashbrown" @@ -1132,15 +1130,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.10.0" @@ -1162,6 +1151,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1195,17 +1190,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" @@ -1217,17 +1201,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1235,7 +1208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -1246,8 +1219,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1263,30 +1236,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.7.0" @@ -1297,8 +1246,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1306,20 +1256,24 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", ] [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "tokio", "tokio-rustls", + "tower-service", ] [[package]] @@ -1328,14 +1282,22 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64 0.22.1", "bytes", + "futures-channel", "futures-core", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.7.0", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1523,7 +1485,7 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.9.2", + "bitflags", "cfg-if", "libc", ] @@ -1534,21 +1496,22 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -1618,7 +1581,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.2", + "bitflags", "libc", "redox_syscall", ] @@ -1634,6 +1597,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "litemap" version = "0.8.0" @@ -1656,6 +1628,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[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.1.0" @@ -1665,12 +1643,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -1699,7 +1671,7 @@ version = "0.1.0" dependencies = [ "argon2", "chrono", - "rand", + "rand 0.8.5", "sea-orm-migration", ] @@ -1719,12 +1691,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1745,16 +1711,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1787,7 +1743,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -1828,6 +1784,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.36.7" @@ -1943,7 +1909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1985,7 +1951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.16", + "thiserror", "ucd-trie", ] @@ -2024,12 +1990,14 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" dependencies = [ "fixedbitset", + "hashbrown 0.15.5", "indexmap 2.10.0", + "serde", ] [[package]] @@ -2119,30 +2087,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2207,6 +2151,61 @@ dependencies = [ "syn 1.0.109", ] +[[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", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "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.40" @@ -2235,8 +2234,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -2246,7 +2255,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2259,26 +2278,34 @@ dependencies = [ ] [[package]] -name = "redis" -version = "0.27.6" +name = "rand_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d8f99a4090c89cc489a94833c901ead69bfbf3877b4867d5482e321ee875bc" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redis" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" dependencies = [ "arc-swap", - "async-trait", "backon", "bytes", + "cfg-if", "combine", - "futures", + "futures-channel", "futures-util", - "itertools", "itoa", "num-bigint", "percent-encoding", "pin-project-lite", "ryu", "sha1_smol", - "socket2 0.5.10", + "socket2", "tokio", "tokio-util", "url", @@ -2290,7 +2317,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.2", + "bitflags", ] [[package]] @@ -2315,9 +2342,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -2368,53 +2395,50 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", + "http", + "http-body", + "http-body-util", + "hyper", "hyper-rustls", - "ipnet", + "hyper-util", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.12", + "quinn", + "rustls", "rustls-native-certs", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration", + "sync_wrapper", "tokio", "tokio-rustls", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", ] [[package]] name = "rhai" -version = "1.22.2" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249" +checksum = "527390cc333a8d2cd8237890e15c36518c26f8b54c903d86fc59f42f08d25594" dependencies = [ "ahash 0.8.12", - "bitflags 2.9.2", + "bitflags", "instant", "num-traits", "once_cell", @@ -2428,9 +2452,9 @@ dependencies = [ [[package]] name = "rhai_codegen" -version = "2.2.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6" dependencies = [ "proc-macro2", "quote", @@ -2487,34 +2511,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.9.2", + "bitflags", "serde", "serde_derive", ] [[package]] name = "rquickjs" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16661bff09e9ed8e01094a188b463de45ec0693ade55b92ed54027d7ba7c40c" +checksum = "5c5227859c4dfc83f428e58f9569bf439e628c8d139020e7faff437e6f5abaa0" dependencies = [ "rquickjs-core", ] [[package]] name = "rquickjs-core" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8db6379e204ef84c0811e90e7cc3e3e4d7688701db68a00d14a6db6849087b" +checksum = "e82e0ca83028ad5b533b53b96c395bbaab905a5774de4aaf1004eeacafa3d85d" dependencies = [ "rquickjs-sys", ] [[package]] name = "rquickjs-sys" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bc352c6b663604c3c186c000cfcc6c271f4b50bc135a285dd6d4f2a42f9790a" +checksum = "7fed0097b0b4fbb2a87f6dd3b995a7c64ca56de30007eb7e867dfdfc78324ba5" dependencies = [ "cc", ] @@ -2532,7 +2556,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -2575,9 +2599,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.20.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" dependencies = [ "cfg-if", "ordered-multimap", @@ -2593,7 +2617,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand", + "rand 0.8.5", "rkyv", "serde", "serde_json", @@ -2606,16 +2630,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] -name = "rustls" -version = "0.21.12" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustls" @@ -2626,51 +2644,33 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pki-types", "schannel", "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pki-types" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.4" @@ -2742,16 +2742,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sea-bae" version = "0.2.1" @@ -2787,7 +2777,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror 2.0.16", + "thiserror", "time", "tracing", "url", @@ -2884,7 +2874,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.106", - "thiserror 2.0.16", + "thiserror", ] [[package]] @@ -2918,11 +2908,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" dependencies = [ - "bitflags 2.9.2", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -2941,18 +2931,40 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -2983,11 +2995,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -3106,9 +3118,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.5" @@ -3123,7 +3141,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.16", + "thiserror", "time", ] @@ -3154,16 +3172,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -3225,19 +3233,19 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.5", - "hashlink 0.10.0", + "hashlink", "indexmap 2.10.0", "log", "memchr", "once_cell", "percent-encoding", "rust_decimal", - "rustls 0.23.31", + "rustls", "serde", "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror", "time", "tokio", "tokio-stream", @@ -3294,7 +3302,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.2", + "bitflags", "byteorder", "bytes", "chrono", @@ -3316,7 +3324,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "rust_decimal", "serde", @@ -3325,7 +3333,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror", "time", "tracing", "uuid", @@ -3341,7 +3349,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.2", + "bitflags", "byteorder", "chrono", "crc", @@ -3360,7 +3368,7 @@ dependencies = [ "memchr", "num-bigint", "once_cell", - "rand", + "rand 0.8.5", "rust_decimal", "serde", "serde_json", @@ -3368,7 +3376,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror", "time", "tracing", "uuid", @@ -3395,7 +3403,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror", "time", "tracing", "url", @@ -3465,17 +3473,14 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[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" @@ -3488,27 +3493,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tap" version = "1.0.1" @@ -3524,33 +3508,13 @@ dependencies = [ "serde", ] -[[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.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.16", -] - -[[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 2.0.106", + "thiserror-impl", ] [[package]] @@ -3653,7 +3617,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.59.0", ] @@ -3671,11 +3635,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ - "rustls 0.21.12", + "rustls", "tokio", ] @@ -3705,14 +3669,15 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" dependencies = [ - "serde", + "serde_core", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", ] [[package]] @@ -3720,8 +3685,14 @@ name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -3731,18 +3702,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.10.0", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", + "toml_datetime 0.6.11", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_parser" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] [[package]] name = "tower" @@ -3753,7 +3724,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -3766,11 +3737,14 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.2", + "bitflags", "bytes", - "http 1.3.1", - "http-body 1.0.1", + "futures-util", + "http", + "http-body", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -3856,6 +3830,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" @@ -3875,19 +3855,19 @@ dependencies = [ "anyhow", "argon2", "async-trait", - "axum 0.8.4", + "axum", "bytes", "chrono", "config", "dotenvy", "futures", - "hyper 1.7.0", + "hyper", "jsonwebtoken", "migration", "once_cell", "percent-encoding", "petgraph", - "rand", + "rand 0.9.2", "redis", "regex", "reqwest", @@ -3899,15 +3879,17 @@ dependencies = [ "serde_with", "serde_yaml", "sha2", - "thiserror 1.0.69", + "thiserror", "tokio", + "tokio-stream", "tower", "tower-http", "tracing", "tracing-subscriber", - "utoipa 5.4.0", + "utoipa", "utoipa-swagger-ui", "uuid", + "wiremock", ] [[package]] @@ -3984,18 +3966,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "utoipa" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" -dependencies = [ - "indexmap 2.10.0", - "serde", - "serde_json", - "utoipa-gen 4.3.1", -] - [[package]] name = "utoipa" version = "5.4.0" @@ -4005,19 +3975,7 @@ dependencies = [ "indexmap 2.10.0", "serde", "serde_json", - "utoipa-gen 5.4.0", -] - -[[package]] -name = "utoipa-gen" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.106", + "utoipa-gen", ] [[package]] @@ -4035,17 +3993,19 @@ dependencies = [ [[package]] name = "utoipa-swagger-ui" -version = "6.0.0" +version = "9.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b39868d43c011961e04b41623e050aedf2cc93652562ff7935ce0f819aaf2da" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" dependencies = [ - "axum 0.7.9", + "axum", + "base64 0.22.1", "mime_guess", "regex", "rust-embed", "serde", "serde_json", - "utoipa 4.2.3", + "url", + "utoipa", "zip", ] @@ -4200,6 +4160,16 @@ dependencies = [ "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-roots" version = "0.26.11" @@ -4557,21 +4527,34 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "winreg" -version = "0.50.0" +name = "wiremock" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "assert-json-diff", + "base64 0.22.1", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", ] [[package]] @@ -4580,7 +4563,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.2", + "bitflags", ] [[package]] @@ -4600,13 +4583,13 @@ dependencies = [ [[package]] name = "yaml-rust2" -version = "0.8.1" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", - "hashlink 0.8.4", + "hashlink", ] [[package]] @@ -4721,12 +4704,32 @@ dependencies = [ [[package]] name = "zip" -version = "0.6.6" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" dependencies = [ - "byteorder", + "arbitrary", "crc32fast", - "crossbeam-utils", "flate2", + "indexmap 2.10.0", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index b75f29d..5eb2a31 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,17 +1,17 @@ [package] name = "udmin" version = "0.1.0" -edition = "2024" # ✅ 升级到最新 Rust Edition +edition = "2024" default-run = "udmin" [dependencies] axum = "0.8.4" tokio = { version = "1.47.1", features = ["full"] } -tower = "0.5.0" +tower = "0.5.2" tower-http = { version = "0.6.6", features = ["cors", "trace"] } hyper = { version = "1" } bytes = "1" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0.225", features = ["derive"] } serde_json = "1.0" serde_with = "3.14.0" sea-orm = { version = "1.1.14", features = ["sqlx-mysql", "sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros"] } @@ -21,30 +21,35 @@ uuid = { version = "1.11.0", features = ["serde", "v4"] } chrono = { version = "0.4", features = ["serde"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } -config = "0.14" -dotenvy = "0.15" -thiserror = "1.0" +config = "0.15.16" +dotenvy = "0.15.7" +thiserror = "2.0.16" anyhow = "1.0" once_cell = "1.19.0" utoipa = { version = "5.4.0", features = ["axum_extras", "chrono", "uuid"] } -utoipa-swagger-ui = { version = "6.0.0", features = ["axum"] } +utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } sha2 = "0.10" -rand = "0.8" -async-trait = "0.1" -redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] } -petgraph = "0.6" -rhai = { version = "1.17", features = ["serde", "metadata", "internals"] } +rand = "0.9.2" +async-trait = "0.1.89" +redis = { version = "0.32.5", features = ["tokio-comp", "connection-manager"] } +petgraph = "0.8.2" +rhai = { version = "1.23.4", features = ["serde", "metadata", "internals"] } serde_yaml = "0.9" -regex = "1.10" -reqwest = { version = "0.11", features = ["json", "rustls-tls-native-roots"], default-features = false } -futures = "0.3" +regex = "1.11.2" +reqwest = { version = "0.12.23", features = ["json", "rustls-tls-native-roots"], default-features = false } +futures = "0.3.31" percent-encoding = "2.3" # 新增: QuickJS 运行时用于 JS 执行器(不启用额外特性) -rquickjs = "0.8" +rquickjs = "0.9.0" +# 新增: 用于将 mpsc::Receiver 封装为 Stream(SSE) +tokio-stream = "0.1.17" [dependencies.migration] path = "migration" [profile.release] lto = true -codegen-units = 1 \ No newline at end of file +codegen-units = 1 + +[dev-dependencies] +wiremock = "0.6" \ No newline at end of file diff --git a/backend/src/flow/context.rs b/backend/src/flow/context.rs index cc12fc5..1c0dffc 100644 --- a/backend/src/flow/context.rs +++ b/backend/src/flow/context.rs @@ -14,14 +14,29 @@ pub enum ExecutionMode { impl Default for ExecutionMode { fn default() -> Self { ExecutionMode::Sync } } +// 新增:流式事件(用于 SSE) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] // 带判别字段,便于前端识别事件类型 +pub enum StreamEvent { + #[serde(rename = "node")] + Node { node_id: String, logs: Vec, ctx: serde_json::Value }, + #[serde(rename = "done")] + Done { ok: bool, ctx: serde_json::Value, logs: Vec }, + #[serde(rename = "error")] + Error { message: String }, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DriveOptions { #[serde(default)] pub max_steps: usize, #[serde(default)] pub execution_mode: ExecutionMode, + // 新增:事件通道(仅运行时使用,不做序列化/反序列化) + #[serde(default, skip_serializing, skip_deserializing)] + pub event_tx: Option>, } impl Default for DriveOptions { - fn default() -> Self { Self { max_steps: 10_000, execution_mode: ExecutionMode::Sync } } + fn default() -> Self { Self { max_steps: 10_000, execution_mode: ExecutionMode::Sync, event_tx: None } } } \ No newline at end of file diff --git a/backend/src/flow/dsl.rs b/backend/src/flow/dsl.rs index 79038b9..eb58b21 100644 --- a/backend/src/flow/dsl.rs +++ b/backend/src/flow/dsl.rs @@ -1,3 +1,12 @@ +//! 模块:流程 DSL 与自由布局 Design JSON 的解析、校验与构建。 +//! 主要内容: +//! - FlowDSL/NodeDSL/EdgeDSL:较为“表述性”的简化 DSL 结构(用于外部接口/入库)。 +//! - DesignSyntax/NodeSyntax/EdgeSyntax:与前端自由布局 JSON 对齐的结构(含 source_port_id 等)。 +//! - validate_design:基础校验(节点 ID 唯一、至少包含一个 start 与一个 end、边的引用合法)。 +//! - build_chain_from_design:将自由布局 JSON 转换为内部 ChainDef(含条件节点 AND 组装等启发式与兼容逻辑)。 +//! - chain_from_design_json:统一入口,支持字符串/对象两种输入,做兼容字段回填后再校验并构建。 +//! 说明:尽量保持向后兼容;在条件节点的出边组装上采用启发式(例如:单出边 + 多条件 => 组装为 AND 条件组)。 + use serde::{Deserialize, Serialize}; use serde_json::Value; use anyhow::bail; @@ -5,36 +14,53 @@ use anyhow::bail; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FlowDSL { #[serde(default)] + /// 流程名称(可选) pub name: String, #[serde(default, alias = "executionMode")] + /// 执行模式(兼容前端 executionMode),如:sync/async(目前仅占位) pub execution_mode: Option, + /// 节点列表(按声明顺序) pub nodes: Vec, #[serde(default)] + /// 边列表(from -> to,可选 condition) pub edges: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeDSL { + /// 节点唯一 ID(字符串) pub id: String, #[serde(default)] + /// 节点类型:start / end / task / condition(开始/结束/任务/条件) pub kind: String, // 节点类型:start / end / task / condition(开始/结束/任务/条件) #[serde(default)] + /// 节点显示名称(可选) pub name: String, #[serde(default)] + /// 任务标识(绑定执行器),如 http/db/variable/script_*(可选) pub task: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EdgeDSL { + /// 起点节点 ID(别名:source/from) #[serde(alias = "source", alias = "from", rename = "from")] pub from: String, + /// 终点节点 ID(别名:target/to) #[serde(alias = "target", alias = "to", rename = "to")] pub to: String, #[serde(default)] + /// 条件表达式(字符串): + /// - 若为 JSON 字符串(以 { 或 [ 开头),则按 JSON 条件集合进行求值; + /// - 否则按 Rhai 表达式求值; + /// - 空字符串/None 表示无条件。 pub condition: Option, } impl From for super::domain::ChainDef { + /// 将简化 DSL 转换为内部 ChainDef: + /// - kind 映射:start/end/condition/其他->task;支持 decision 别名 -> condition。 + /// - 直接搬运 edges 的 from/to/condition。 fn from(v: FlowDSL) -> Self { super::domain::ChainDef { name: v.name, @@ -71,34 +97,47 @@ impl From for super::domain::ChainDef { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DesignSyntax { #[serde(default)] + /// 设计名称(可选) pub name: String, #[serde(default)] + /// 节点集合(自由布局) pub nodes: Vec, #[serde(default)] + /// 边集合(自由布局) pub edges: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeSyntax { + /// 节点 ID pub id: String, #[serde(rename = "type", default)] + /// 前端类型:start | end | condition | http | db | task | script_*(用于推断具体执行器) pub kind: String, // 取值: start | end | condition | http | db | task(开始/结束/条件/HTTP/数据库/通用任务) #[serde(default)] + /// 节点附加数据:title/conditions/scripts 等 pub data: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EdgeSyntax { + /// 起点(兼容 sourceNodeID/source/from) #[serde(alias = "sourceNodeID", alias = "source", alias = "from")] pub from: String, + /// 终点(兼容 targetNodeID/target/to) #[serde(alias = "targetNodeID", alias = "target", alias = "to")] pub to: String, #[serde(default)] + /// 源端口 ID:用于条件节点端口到条件 key 的兼容映射; + /// 特殊值 and/all/group/true 表示将节点内所有 conditions 的 value 组装为 AND 组。 pub source_port_id: Option, } -/// 从 design_json(前端流程 JSON)构建 ChainDef +/// 设计级别校验: +/// - 节点 ID 唯一且非空; +/// - 至少一个 start 与一个 end; +/// - 边的 from/to 必须指向已知节点。 fn validate_design(design: &DesignSyntax) -> anyhow::Result<()> { use std::collections::HashSet; let mut ids = HashSet::new(); @@ -129,6 +168,13 @@ fn validate_design(design: &DesignSyntax) -> anyhow::Result<()> { Ok(()) } +/// 将自由布局 DesignSyntax 转换为内部 ChainDef: +/// - 节点:推断 kind/name/task(含 scripts 与 inline script/expr 的兼容); +/// - 边: +/// * 条件节点:支持 source_port_id 到 data.conditions 的旧版映射; +/// * 当 source_port_id 为空或为 and/all/group/true,取 conditions 的 value 组成 AND 组; +/// * 启发式:若条件节点仅一条出边且包含多个 conditions,即便 source_port_id 指向具体 key,也回退为 AND 组; +/// * 非条件节点:不处理条件。 fn build_chain_from_design(design: &DesignSyntax) -> anyhow::Result { use super::domain::{ChainDef, NodeDef, NodeId, NodeKind, LinkDef}; diff --git a/backend/src/flow/engine.rs b/backend/src/flow/engine.rs index 2bed11e..abde915 100644 --- a/backend/src/flow/engine.rs +++ b/backend/src/flow/engine.rs @@ -11,7 +11,17 @@ use std::cell::RefCell; use rhai::AST; use regex::Regex; -// 将常用的正则匹配暴露给表达式使用 +// 模块:流程执行引擎(engine.rs) +// 作用:驱动 ChainDef 流程图,支持: +// - 同步/异步(Fire-and-Forget)任务执行 +// - 条件路由(Rhai 表达式与 JSON 条件)与无条件回退 +// - 并发分支 fan-out 与 join_all 等待 +// - SSE 实时事件推送(逐行增量 + 节点级切片) +// 设计要点: +// - 表达式执行使用 thread_local 的 Rhai Engine 与 AST 缓存,避免全局 Send/Sync 限制 +// - 共享上下文使用 RwLock 包裹 serde_json::Value;日志聚合使用 Mutex> +// - 不做冲突校验:允许并发修改;最后写回/写入按代码路径覆盖 +// fn regex_match(s: &str, pat: &str) -> bool { Regex::new(pat).map(|re| re.is_match(s)).unwrap_or(false) } @@ -135,7 +145,7 @@ impl FlowEngine { pub fn builder() -> FlowEngineBuilder { FlowEngineBuilder::default() } pub async fn drive(&self, chain: &ChainDef, ctx: serde_json::Value, opts: DriveOptions) -> anyhow::Result<(serde_json::Value, Vec)> { - // 1) 选取起点 + // 1) 选取起点:优先 Start;否则入度为 0;再否则第一个节点 // 查找 start:优先 Start 节点;否则选择入度为 0 的第一个节点;再否则回退第一个节点 let start = if let Some(n) = chain .nodes @@ -208,10 +218,30 @@ async fn drive_from( // 进入节点:打点 let node_id_str = node.id.0.clone(); let node_start = Instant::now(); - { - let mut lg = logs.lock().await; - lg.push(format!("enter node: {}", node.id.0)); + // 进入节点前记录当前日志长度,便于节点结束时做切片 + let pre_len = { logs.lock().await.len() }; + // 在每次追加日志时同步发送一条增量 SSE 事件(仅 1 行日志),以提升实时性 + // push_and_emit: + // - 先将单行日志 push 到共享日志 + // - 若存在 SSE 通道,截取上下文快照并发送单行增量事件 + async fn push_and_emit( + logs: &std::sync::Arc>>, + opts: &super::context::DriveOptions, + node_id: &str, + ctx: &std::sync::Arc>, + msg: String, + ) { + { + let mut lg = logs.lock().await; + lg.push(msg.clone()); + } + if let Some(tx) = opts.event_tx.as_ref() { + let ctx_snapshot = { ctx.read().await.clone() }; + crate::middlewares::sse::emit_node(&tx, node_id.to_string(), vec![msg], ctx_snapshot).await; + } } + // enter 节点也实时推送 + push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("enter node: {}", node.id.0)).await; info!(target: "udmin.flow", "enter node: {}", node.id.0); // 执行任务 @@ -221,13 +251,21 @@ async fn drive_from( ExecutionMode::Sync => { // 使用快照执行,结束后整体写回(允许最后写入覆盖并发修改;程序端不做冲突校验) let mut local_ctx = { ctx.read().await.clone() }; - task.execute(&node.id, node, &mut local_ctx).await?; - { let mut w = ctx.write().await; *w = local_ctx; } - { - let mut lg = logs.lock().await; - lg.push(format!("exec task: {} (sync)", task_name)); + match task.execute(&node.id, node, &mut local_ctx).await { + Ok(_) => { + { let mut w = ctx.write().await; *w = local_ctx; } + push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("exec task: {} (sync)", task_name)).await; + info!(target: "udmin.flow", "exec task: {} (sync)", task_name); + } + Err(e) => { + let err_msg = format!("task error: {}: {}", task_name, e); + push_and_emit(&logs, &opts, &node_id_str, &ctx, err_msg.clone()).await; + // 捕获快照并返回 DriveError + let ctx_snapshot = { ctx.read().await.clone() }; + let logs_snapshot = { logs.lock().await.clone() }; + return Err(anyhow::Error::new(DriveError { node_id: node_id_str.clone(), ctx: ctx_snapshot, logs: logs_snapshot, message: err_msg })); + } } - info!(target: "udmin.flow", "exec task: {} (sync)", task_name); } ExecutionMode::AsyncFireAndForget => { // fire-and-forget:基于快照执行,不写回共享 ctx(变量任务除外:做有界差异写回) @@ -238,6 +276,7 @@ async fn drive_from( let node_def = node.clone(); let logs_clone = logs.clone(); let ctx_clone = ctx.clone(); + let event_tx_opt = opts.event_tx.clone(); tokio::spawn(async move { let mut c = task_ctx.clone(); let _ = task_arc.execute(&node_id, &node_def, &mut c).await; @@ -268,25 +307,31 @@ async fn drive_from( let mut lg = logs_clone.lock().await; lg.push(format!("exec task done (async): {} (writeback variable)", name_for_log)); } + // 实时推送异步完成日志 + if let Some(tx) = event_tx_opt.as_ref() { + let ctx_snapshot = { ctx_clone.read().await.clone() }; + crate::middlewares::sse::emit_node(&tx, node_id.0.clone(), vec![format!("exec task done (async): {} (writeback variable)", name_for_log)], ctx_snapshot).await; + } info!(target: "udmin.flow", "exec task done (async): {} (writeback variable)", name_for_log); } else { { let mut lg = logs_clone.lock().await; lg.push(format!("exec task done (async): {}", name_for_log)); } + // 实时推送异步完成日志 + if let Some(tx) = event_tx_opt.as_ref() { + let ctx_snapshot = { ctx_clone.read().await.clone() }; + crate::middlewares::sse::emit_node(&tx, node_id.0.clone(), vec![format!("exec task done (async): {}", name_for_log)], ctx_snapshot).await; + } info!(target: "udmin.flow", "exec task done (async): {}", name_for_log); } }); - { - let mut lg = logs.lock().await; - lg.push(format!("spawn task: {} (async)", task_name)); - } + push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("spawn task: {} (async)", task_name)).await; info!(target: "udmin.flow", "spawn task: {} (async)", task_name); } } } else { - let mut lg = logs.lock().await; - lg.push(format!("task not found: {} (skip)", task_name)); + push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("task not found: {} (skip)", task_name)).await; info!(target: "udmin.flow", "task not found: {} (skip)", task_name); } } @@ -294,11 +339,13 @@ async fn drive_from( // End 节点:记录耗时后结束 if matches!(node.kind, NodeKind::End) { let duration = node_start.elapsed().as_millis(); - { - let mut lg = logs.lock().await; - lg.push(format!("leave node: {} {} ms", node_id_str, duration)); - } + push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("leave node: {} {} ms", node_id_str, duration)).await; info!(target: "udmin.flow", "leave node: {} {} ms", node_id_str, duration); + if let Some(tx) = opts.event_tx.as_ref() { + let node_logs = { let lg = logs.lock().await; lg[pre_len..].to_vec() }; + let ctx_snapshot = { ctx.read().await.clone() }; + crate::middlewares::sse::emit_node(&tx, node_id_str.clone(), node_logs, ctx_snapshot).await; + } break; } @@ -367,7 +414,13 @@ async fn drive_from( let mut lg = logs.lock().await; lg.push(format!("leave node: {} {} ms", node_id_str, duration)); } + push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("leave node: {} {} ms", node_id_str, duration)).await; info!(target: "udmin.flow", "leave node: {} {} ms", node_id_str, duration); + if let Some(tx) = opts.event_tx.as_ref() { + let node_logs = { let lg = logs.lock().await; lg[pre_len..].to_vec() }; + let ctx_snapshot = { ctx.read().await.clone() }; + crate::middlewares::sse::emit_node(&tx, node_id_str.clone(), node_logs, ctx_snapshot).await; + } break; } @@ -379,6 +432,11 @@ async fn drive_from( lg.push(format!("leave node: {} {} ms", node_id_str, duration)); } info!(target: "udmin.flow", "leave node: {} {} ms", node_id_str, duration); + if let Some(tx) = opts.event_tx.as_ref() { + let node_logs = { let lg = logs.lock().await; lg[pre_len..].to_vec() }; + let ctx_snapshot = { ctx.read().await.clone() }; + crate::middlewares::sse::emit_node(&tx, node_id_str.clone(), node_logs, ctx_snapshot).await; + } current = nexts.remove(0); continue; } @@ -405,6 +463,11 @@ async fn drive_from( lg.push(format!("leave node: {} {} ms", node_id_str, duration)); } info!(target: "udmin.flow", "leave node: {} {} ms", node_id_str, duration); + if let Some(tx) = opts.event_tx.as_ref() { + let node_logs = { let lg = logs.lock().await; lg[pre_len..].to_vec() }; + let ctx_snapshot = { ctx.read().await.clone() }; + crate::middlewares::sse::emit_node(&tx, node_id_str.clone(), node_logs, ctx_snapshot).await; + } } Ok(()) @@ -427,138 +490,19 @@ impl Default for FlowEngine { fn default() -> Self { Self { tasks: crate::flow::task::default_registry() } } } -/* moved to executors::condition -fn eval_condition_json(ctx: &serde_json::Value, cond: &serde_json::Value) -> anyhow::Result { --fn eval_condition_json(ctx: &serde_json::Value, cond: &serde_json::Value) -> anyhow::Result { -- // 支持前端 Condition 组件导出的: { left:{type, content}, operator, right? } -- use serde_json::Value as V; -- -- let left = cond.get("left").ok_or_else(|| anyhow::anyhow!("missing left"))?; -- let op_raw = cond.get("operator").and_then(|v| v.as_str()).unwrap_or(""); -- let right = cond.get("right"); -- -- let lval = resolve_value(ctx, left)?; -- let rval = match right { Some(v) => Some(resolve_value(ctx, v)?), None => None }; -- -- // 归一化操作符:忽略大小写,替换下划线为空格 -- let op = op_raw.trim().to_lowercase().replace('_', " "); -- -- // 工具函数 -- fn to_f64(v: &V) -> Option { -- match v { -- V::Number(n) => n.as_f64(), -- V::String(s) => s.parse::().ok(), -- _ => None, -- } -- } -- fn is_empty_val(v: &V) -> bool { -- match v { -- V::Null => true, -- V::String(s) => s.trim().is_empty(), -- V::Array(a) => a.is_empty(), -- V::Object(m) => m.is_empty(), -- _ => false, -- } -- } -- fn json_equal(a: &V, b: &V) -> bool { -- match (a, b) { -- (V::Number(_), V::Number(_)) | (V::Number(_), V::String(_)) | (V::String(_), V::Number(_)) => { -- match (to_f64(a), to_f64(b)) { (Some(x), Some(y)) => x == y, _ => a == b } -- } -- _ => a == b, -- } -- } -- fn contains(left: &V, right: &V) -> bool { -- match (left, right) { -- (V::String(s), V::String(t)) => s.contains(t), -- (V::Array(arr), r) => arr.iter().any(|x| json_equal(x, r)), -- (V::Object(map), V::String(key)) => map.contains_key(key), -- _ => false, -- } -- } -- fn in_op(left: &V, right: &V) -> bool { -- match right { -- V::Array(arr) => arr.iter().any(|x| json_equal(left, x)), -- V::Object(map) => match left { V::String(k) => map.contains_key(k), _ => false }, -- V::String(hay) => match left { V::String(needle) => hay.contains(needle), _ => false }, -- _ => false, -- } -- } -- fn bool_like(v: &V) -> bool { -- match v { -- V::Bool(b) => *b, -- V::Null => false, -- V::Number(n) => n.as_f64().map(|x| x != 0.0).unwrap_or(false), -- V::String(s) => { -- let s_l = s.trim().to_lowercase(); -- if s_l == "true" { true } else if s_l == "false" { false } else { !s_l.is_empty() } -- } -- V::Array(a) => !a.is_empty(), -- V::Object(m) => !m.is_empty(), -- } -- } -- -- let res = match (op.as_str(), &lval, &rval) { -- // 等于 / 不等于(适配所有 JSON 类型;数字按 f64 比较,其他走深度相等) -- ("equal" | "equals" | "==" | "eq", l, Some(r)) => json_equal(l, r), -- ("not equal" | "!=" | "not equals" | "neq", l, Some(r)) => !json_equal(l, r), -- -- // 数字比较 -- ("greater than" | ">" | "gt", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a > b, _ => false }, -- ("greater than or equal" | ">=" | "gte" | "ge", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a >= b, _ => false }, -- ("less than" | "<" | "lt", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a < b, _ => false }, -- ("less than or equal" | "<=" | "lte" | "le", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a <= b, _ => false }, -- -- // 包含 / 不包含(字符串、数组、对象(键)) -- ("contains", l, Some(r)) => contains(l, r), -- ("not contains", l, Some(r)) => !contains(l, r), -- -- // 成员关系:left in right / not in -- ("in", l, Some(r)) => in_op(l, r), -- ("not in" | "nin", l, Some(r)) => !in_op(l, r), -- -- // 为空 / 非空(字符串、数组、对象、null) -- ("is empty" | "empty" | "isempty", l, _) => is_empty_val(l), -- ("is not empty" | "not empty" | "notempty", l, _) => !is_empty_val(l), -- -- // 布尔判断(对各类型进行布尔化) -- ("is true" | "is true?" | "istrue", l, _) => bool_like(l), -- ("is false" | "isfalse", l, _) => !bool_like(l), -- -- _ => false, -- }; -- Ok(res) --} -- --fn resolve_value(ctx: &serde_json::Value, v: &serde_json::Value) -> anyhow::Result { -- use serde_json::Value as V; -- let t = v.get("type").and_then(|v| v.as_str()).unwrap_or(""); -- match t { -- "constant" => Ok(v.get("content").cloned().unwrap_or(V::Null)), -- "ref" => { -- // content: [nodeId, field] -- if let Some(arr) = v.get("content").and_then(|v| v.as_array()) { -- if arr.len() >= 2 { -- if let (Some(node), Some(field)) = (arr[0].as_str(), arr[1].as_str()) { -- let val = ctx -- .get("nodes") -- .and_then(|n| n.get(node)) -- .and_then(|m| m.get(field)) -- .cloned() -- .or_else(|| ctx.get(field).cloned()) -- .unwrap_or(V::Null); -- return Ok(val); -- } -- } -- } -- Ok(V::Null) -- } -- "expression" => { -- let expr = v.get("content").and_then(|x| x.as_str()).unwrap_or(""); -- if expr.trim().is_empty() { return Ok(V::Null); } -- Ok(super::engine::eval_rhai_expr_json(expr, ctx).unwrap_or(V::Null)) -- } -- _ => Ok(V::Null), + +#[derive(Debug, Clone)] +pub struct DriveError { + pub node_id: String, + pub ctx: serde_json::Value, + pub logs: Vec, + pub message: String, +} + +impl std::fmt::Display for DriveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) } } -*/ \ No newline at end of file + +impl std::error::Error for DriveError {} \ No newline at end of file diff --git a/backend/src/flow/executors/http.rs b/backend/src/flow/executors/http.rs index 56f835c..4a2c351 100644 --- a/backend/src/flow/executors/http.rs +++ b/backend/src/flow/executors/http.rs @@ -2,8 +2,7 @@ use async_trait::async_trait; use serde_json::{Value, json, Map}; use tracing::info; use std::collections::HashMap; -use std::time::Duration; -use reqwest::Certificate; +use crate::middlewares::http_client::{execute_http, HttpClientOptions, HttpRequest}; use crate::flow::task::Executor; use crate::flow::domain::{NodeDef, NodeId}; @@ -34,64 +33,29 @@ impl Executor for HttpTask { return Ok(()); }; - // 3) 解析配置 + // 3) 解析配置 -> 转换为中间件请求参数 let (method, url, headers, query, body, opts) = parse_http_config(cfg)?; info!(target = "udmin.flow", "http task: {} {}", method, url); - // 4) 发送请求(支持 HTTPS 相关选项) - let client = { - let mut builder = reqwest::Client::builder(); - if let Some(ms) = opts.timeout_ms { builder = builder.timeout(Duration::from_millis(ms)); } - if opts.insecure { builder = builder.danger_accept_invalid_certs(true); } - if opts.http1_only { builder = builder.http1_only(); } - if let Some(pem) = opts.ca_pem { - if let Ok(cert) = Certificate::from_pem(pem.as_bytes()) { - builder = builder.add_root_certificate(cert); - } - } - builder.build()? + let req = HttpRequest { + method, + url, + headers, + query, + body, + }; + let client_opts = HttpClientOptions { + timeout_ms: opts.timeout_ms, + insecure: opts.insecure, + ca_pem: opts.ca_pem, + http1_only: opts.http1_only, }; - let mut req = client.request(method.parse()?, url); - if let Some(hs) = headers { - use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; - let mut map = HeaderMap::new(); - for (k, v) in hs { - if let (Ok(name), Ok(value)) = (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v)) { - map.insert(name, value); - } - } - req = req.headers(map); - } - - if let Some(qs) = query { - // 将查询参数转成 (String, String) 列表,便于 reqwest 序列化 - let mut pairs: Vec<(String, String)> = Vec::new(); - for (k, v) in qs { - let s = match v { - Value::String(s) => s, - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - other => other.to_string(), - }; - pairs.push((k, s)); - } - req = req.query(&pairs); - } - - if let Some(b) = body { req = req.json(&b); } - - let resp = req.send().await?; - let status = resp.status().as_u16(); - let headers_out: Map = resp - .headers() - .iter() - .map(|(k, v)| (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))) - .collect(); - - // 尝试以 JSON 解析,否则退回文本 - let text = resp.text().await?; - let parsed_body: Value = serde_json::from_str(&text).unwrap_or_else(|_| Value::String(text)); + // 4) 调用中间件发送请求 + let out = execute_http(req, client_opts).await?; + let status = out.status; + let headers_out = out.headers; + let parsed_body = out.body; // 5) 将结果写回 ctx let result = json!({ @@ -138,8 +102,15 @@ fn parse_http_config(cfg: Value) -> anyhow::Result<( let query = m.remove("query").and_then(|v| v.as_object().cloned()); let body = m.remove("body"); - // 可选 HTTPS/超时/HTTP 版本配置 - let timeout_ms = m.remove("timeout_ms").and_then(|v| v.as_u64()); + // 统一解析超时配置(内联) + let timeout_ms = if let Some(ms) = m.remove("timeout_ms").and_then(|v| v.as_u64()) { + Some(ms) + } else if let Some(Value::Object(mut to)) = m.remove("timeout") { + to.remove("timeout").and_then(|v| v.as_u64()) + } else { + None + }; + let insecure = m.remove("insecure").and_then(|v| v.as_bool()).unwrap_or(false); let http1_only = m.remove("http1_only").and_then(|v| v.as_bool()).unwrap_or(false); let ca_pem = m.remove("ca_pem").and_then(|v| v.as_str().map(|s| s.to_string())); diff --git a/backend/src/flow/executors/mod.rs b/backend/src/flow/executors/mod.rs index de4b6fa..084ea3d 100644 --- a/backend/src/flow/executors/mod.rs +++ b/backend/src/flow/executors/mod.rs @@ -1,6 +1,5 @@ pub mod http; pub mod db; -// removed: pub mod expr; pub mod variable; pub mod script_rhai; pub mod script_js; diff --git a/backend/src/flow/log_handler.rs b/backend/src/flow/log_handler.rs new file mode 100644 index 0000000..294cd4e --- /dev/null +++ b/backend/src/flow/log_handler.rs @@ -0,0 +1,219 @@ +use async_trait::async_trait; +use chrono::{DateTime, FixedOffset}; +use serde_json::Value; +use tokio::sync::mpsc::Sender; + +use crate::flow::context::StreamEvent; +use crate::services::flow_run_log_service::{self, CreateRunLogInput}; +use crate::db::Db; + +/// 流程执行日志处理器抽象接口 +#[async_trait] +pub trait FlowLogHandler: Send + Sync { + /// 记录流程开始执行 + async fn log_start(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, operator: Option<(i64, String)>) -> anyhow::Result<()>; + + /// 记录流程执行失败(仅包含错误信息) + async fn log_error(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, error_msg: &str, operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()>; + + /// 记录流程执行失败(包含部分输出与累计日志) + async fn log_error_detail(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, output: &Value, logs: &[String], error_msg: &str, operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()> { + // 默认实现:退化为仅错误信息 + self.log_error(flow_id, flow_code, input, error_msg, operator, started_at, duration_ms).await + } + + /// 记录流程执行成功 + async fn log_success(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, output: &Value, logs: &[String], operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()>; + + /// 推送节点执行事件(仅SSE实现需要) + async fn emit_node_event(&self, _node_id: &str, _event_type: &str, _data: &Value) -> anyhow::Result<()> { + // 默认空实现,数据库日志处理器不需要 + Ok(()) + } + + /// 推送完成事件(仅SSE实现需要) + async fn emit_done(&self, _success: bool, _output: &Value, _logs: &[String]) -> anyhow::Result<()> { + // 默认空实现,数据库日志处理器不需要 + Ok(()) + } +} + +/// 数据库日志处理器 +pub struct DatabaseLogHandler { + db: Db, +} + +impl DatabaseLogHandler { + pub fn new(db: Db) -> Self { + Self { db } + } +} + +#[async_trait] +impl FlowLogHandler for DatabaseLogHandler { + async fn log_start(&self, _flow_id: &str, _flow_code: Option<&str>, _input: &Value, _operator: Option<(i64, String)>) -> anyhow::Result<()> { + // 数据库日志处理器不需要记录开始事件,只在结束时记录 + Ok(()) + } + + async fn log_error(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, error_msg: &str, operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()> { + let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None)); + flow_run_log_service::create(&self.db, CreateRunLogInput { + flow_id: flow_id.to_string(), + flow_code: flow_code.map(|s| s.to_string()), + input: Some(serde_json::to_string(input).unwrap_or_default()), + output: None, + ok: false, + logs: Some(error_msg.to_string()), + user_id, + username, + started_at, + duration_ms, + }).await.map_err(|e| anyhow::anyhow!("Failed to create error log: {}", e))?; + Ok(()) + } + + async fn log_error_detail(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, output: &Value, logs: &[String], error_msg: &str, operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()> { + let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None)); + // 将 error_msg 附加到日志尾部(若最后一条不同),确保日志中有清晰的错误描述且不重复 + let mut all_logs = logs.to_vec(); + if all_logs.last().map(|s| s != error_msg).unwrap_or(true) { + all_logs.push(error_msg.to_string()); + } + flow_run_log_service::create(&self.db, CreateRunLogInput { + flow_id: flow_id.to_string(), + flow_code: flow_code.map(|s| s.to_string()), + input: Some(serde_json::to_string(input).unwrap_or_default()), + output: Some(serde_json::to_string(output).unwrap_or_default()), + ok: false, + logs: Some(serde_json::to_string(&all_logs).unwrap_or_default()), + user_id, + username, + started_at, + duration_ms, + }).await.map_err(|e| anyhow::anyhow!("Failed to create error log with details: {}", e))?; + Ok(()) + } + + async fn log_success(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, output: &Value, logs: &[String], operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()> { + let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None)); + flow_run_log_service::create(&self.db, CreateRunLogInput { + flow_id: flow_id.to_string(), + flow_code: flow_code.map(|s| s.to_string()), + input: Some(serde_json::to_string(input).unwrap_or_default()), + output: Some(serde_json::to_string(output).unwrap_or_default()), + ok: true, + logs: Some(serde_json::to_string(logs).unwrap_or_default()), + user_id, + username, + started_at, + duration_ms, + }).await.map_err(|e| anyhow::anyhow!("Failed to create success log: {}", e))?; + Ok(()) + } +} + +/// SSE日志处理器 +pub struct SseLogHandler { + db: Db, + event_tx: Sender, +} + +impl SseLogHandler { + pub fn new(db: Db, event_tx: Sender) -> Self { + Self { db, event_tx } + } +} + +#[async_trait] +impl FlowLogHandler for SseLogHandler { + async fn log_start(&self, _flow_id: &str, _flow_code: Option<&str>, _input: &Value, _operator: Option<(i64, String)>) -> anyhow::Result<()> { + // SSE处理器也不需要记录开始事件 + Ok(()) + } + + async fn log_error(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, error_msg: &str, operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()> { + // 先推送SSE错误事件(不在此处发送 done,交由调用方统一携带 ctx/logs 发送) + crate::middlewares::sse::emit_error(&self.event_tx, error_msg.to_string()).await; + + // 然后记录到数据库(仅错误信息) + let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None)); + flow_run_log_service::create(&self.db, CreateRunLogInput { + flow_id: flow_id.to_string(), + flow_code: flow_code.map(|s| s.to_string()), + input: Some(serde_json::to_string(input).unwrap_or_default()), + output: None, + ok: false, + logs: Some(error_msg.to_string()), + user_id, + username, + started_at, + duration_ms, + }).await.map_err(|e| anyhow::anyhow!("Failed to create error log: {}", e))?; + Ok(()) + } + + async fn log_error_detail(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, output: &Value, logs: &[String], error_msg: &str, operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()> { + // 先推送SSE错误事件(不在此处发送 done,交由调用方统一携带 ctx/logs 发送) + crate::middlewares::sse::emit_error(&self.event_tx, error_msg.to_string()).await; + + // 然后记录到数据库(包含部分输出与累计日志),避免重复附加相同错误信息 + let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None)); + let mut all_logs = logs.to_vec(); + if all_logs.last().map(|s| s != error_msg).unwrap_or(true) { + all_logs.push(error_msg.to_string()); + } + flow_run_log_service::create(&self.db, CreateRunLogInput { + flow_id: flow_id.to_string(), + flow_code: flow_code.map(|s| s.to_string()), + input: Some(serde_json::to_string(input).unwrap_or_default()), + output: Some(serde_json::to_string(output).unwrap_or_default()), + ok: false, + logs: Some(serde_json::to_string(&all_logs).unwrap_or_default()), + user_id, + username, + started_at, + duration_ms, + }).await.map_err(|e| anyhow::anyhow!("Failed to create error log with details: {}", e))?; + Ok(()) + } + + async fn log_success(&self, flow_id: &str, flow_code: Option<&str>, input: &Value, output: &Value, logs: &[String], operator: Option<(i64, String)>, started_at: DateTime, duration_ms: i64) -> anyhow::Result<()> { + // 先推送SSE完成事件 + crate::middlewares::sse::emit_done(&self.event_tx, true, output.clone(), logs.to_vec()).await; + + // 然后记录到数据库 + let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None)); + flow_run_log_service::create(&self.db, CreateRunLogInput { + flow_id: flow_id.to_string(), + flow_code: flow_code.map(|s| s.to_string()), + input: Some(serde_json::to_string(input).unwrap_or_default()), + output: Some(serde_json::to_string(output).unwrap_or_default()), + ok: true, + logs: Some(serde_json::to_string(logs).unwrap_or_default()), + user_id, + username, + started_at, + duration_ms, + }).await.map_err(|e| anyhow::anyhow!("Failed to create success log: {}", e))?; + Ok(()) + } + + async fn emit_node_event(&self, node_id: &str, event_type: &str, data: &Value) -> anyhow::Result<()> { + // 推送节点事件到SSE + let event = StreamEvent::Node { + node_id: node_id.to_string(), + logs: vec![event_type.to_string()], + ctx: data.clone(), + }; + if let Err(_e) = self.event_tx.send(event).await { + // 通道可能已关闭,忽略错误 + } + Ok(()) + } + + async fn emit_done(&self, success: bool, output: &Value, logs: &[String]) -> anyhow::Result<()> { + crate::middlewares::sse::emit_done(&self.event_tx, success, output.clone(), logs.to_vec()).await; + Ok(()) + } +} \ No newline at end of file diff --git a/backend/src/flow/mappers/http.rs b/backend/src/flow/mappers/http.rs index 6ce7acc..bcc907d 100644 --- a/backend/src/flow/mappers/http.rs +++ b/backend/src/flow/mappers/http.rs @@ -1,6 +1,6 @@ use serde_json::Value; -// Extract http config: method, url, headers, query, body from a node +// 从节点中提取 HTTP 配置:method、url、headers、query、body pub fn extract_http_cfg(n: &Value) -> Option { let data = n.get("data"); let api = data.and_then(|d| d.get("api")); @@ -28,7 +28,7 @@ pub fn extract_http_cfg(n: &Value) -> Option { http_obj.insert("method".into(), Value::String(method)); http_obj.insert("url".into(), Value::String(url)); - // Optional: headers + // 可选:headers if let Some(hs) = api.and_then(|a| a.get("headers")).and_then(|v| v.as_object()) { let mut heads = serde_json::Map::new(); for (k, v) in hs.iter() { @@ -41,7 +41,7 @@ pub fn extract_http_cfg(n: &Value) -> Option { } } - // Optional: query + // 可选:query if let Some(qs) = api.and_then(|a| a.get("query")).and_then(|v| v.as_object()) { let mut query = serde_json::Map::new(); for (k, v) in qs.iter() { @@ -52,7 +52,7 @@ pub fn extract_http_cfg(n: &Value) -> Option { } } - // Optional: body + // 可选:body if let Some(body_obj) = data.and_then(|d| d.get("body")).and_then(|v| v.as_object()) { if let Some(Value::Object(json_body)) = body_obj.get("json") { http_obj.insert("body".into(), Value::Object(json_body.clone())); @@ -61,5 +61,28 @@ pub fn extract_http_cfg(n: &Value) -> Option { } } + // 可选:超时(统一处理:数字或对象) + if let Some(to_val) = data.and_then(|d| d.get("timeout")) { + match to_val { + Value::Number(n) => { + http_obj.insert("timeout_ms".into(), Value::Number(n.clone())); + } + Value::Object(obj) => { + // 只读访问对象中的字段并规范化 + let mut t = serde_json::Map::new(); + if let Some(ms) = obj.get("timeout").and_then(|v| v.as_u64()) { + t.insert("timeout".into(), Value::Number(serde_json::Number::from(ms))); + } + if let Some(rt) = obj.get("retryTimes").and_then(|v| v.as_u64()) { + t.insert("retryTimes".into(), Value::Number(serde_json::Number::from(rt))); + } + if !t.is_empty() { + http_obj.insert("timeout".into(), Value::Object(t)); + } + } + _ => {} + } + } + Some(Value::Object(http_obj)) } \ No newline at end of file diff --git a/backend/src/flow/mod.rs b/backend/src/flow/mod.rs index aa22d97..add3a4b 100644 --- a/backend/src/flow/mod.rs +++ b/backend/src/flow/mod.rs @@ -3,6 +3,6 @@ pub mod context; pub mod task; pub mod engine; pub mod dsl; -// removed: pub mod storage; pub mod executors; -pub mod mappers; \ No newline at end of file +pub mod mappers; +pub mod log_handler; \ No newline at end of file diff --git a/backend/src/middlewares/http_client.rs b/backend/src/middlewares/http_client.rs new file mode 100644 index 0000000..d84e1ad --- /dev/null +++ b/backend/src/middlewares/http_client.rs @@ -0,0 +1,178 @@ +use std::collections::HashMap; +use std::time::Duration; + +use anyhow::Result; +use reqwest::Certificate; +use serde_json::{Map, Value}; + +#[derive(Debug, Clone, Default)] +pub struct HttpClientOptions { + pub timeout_ms: Option, + pub insecure: bool, + pub ca_pem: Option, + pub http1_only: bool, +} + +#[derive(Debug, Clone, Default)] +pub struct HttpRequest { + pub method: String, + pub url: String, + pub headers: Option>, // header values are strings + pub query: Option>, // query values will be stringified + pub body: Option, // json body +} + +#[derive(Debug, Clone)] +pub struct HttpResponse { + pub status: u16, + pub headers: Map, + pub body: Value, +} + +pub async fn execute_http(req: HttpRequest, opts: HttpClientOptions) -> Result { + // Build client with options + let mut builder = reqwest::Client::builder(); + if let Some(ms) = opts.timeout_ms { + builder = builder.timeout(Duration::from_millis(ms)); + } + if opts.insecure { + builder = builder.danger_accept_invalid_certs(true); + } + if opts.http1_only { + builder = builder.http1_only(); + } + if let Some(pem) = opts.ca_pem { + if let Ok(cert) = Certificate::from_pem(pem.as_bytes()) { + builder = builder.add_root_certificate(cert); + } + } + let client = builder.build()?; + + // Build request + let mut rb = client.request(req.method.parse()?, req.url); + + // Also set per-request timeout to ensure it takes effect in all cases + if let Some(ms) = opts.timeout_ms { + rb = rb.timeout(Duration::from_millis(ms)); + } + + if let Some(hs) = req.headers { + use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; + let mut map = HeaderMap::new(); + for (k, v) in hs { + if let (Ok(name), Ok(value)) = (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v)) { + map.insert(name, value); + } + } + rb = rb.headers(map); + } + + if let Some(qs) = req.query { + let mut pairs: Vec<(String, String)> = Vec::new(); + for (k, v) in qs { + let s = match v { + Value::String(s) => s, + Value::Number(n) => n.to_string(), + Value::Bool(b) => b.to_string(), + other => other.to_string(), + }; + pairs.push((k, s)); + } + rb = rb.query(&pairs); + } + + if let Some(b) = req.body { + rb = rb.json(&b); + } + + let resp = rb.send().await?; + let status = resp.status().as_u16(); + let headers_out: Map = resp + .headers() + .iter() + .map(|(k, v)| (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))) + .collect(); + + let text = resp.text().await?; + let parsed_body: Value = serde_json::from_str(&text).unwrap_or_else(|_| Value::String(text)); + + Ok(HttpResponse { + status, + headers: headers_out, + body: parsed_body, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + #[tokio::test] + async fn test_get_success() { + let server = MockServer::start().await; + let body = serde_json::json!({"ok": true}); + Mock::given(method("GET")) + .and(path("/hello")) + .respond_with(ResponseTemplate::new(200).set_body_json(body.clone())) + .mount(&server) + .await; + + let req = HttpRequest { + method: "GET".into(), + url: format!("{}/hello", server.uri()), + ..Default::default() + }; + let opts = HttpClientOptions::default(); + let resp = execute_http(req, opts).await.unwrap(); + assert_eq!(resp.status, 200); + assert_eq!(resp.body, body); + } + + #[tokio::test] + async fn test_post_json() { + let server = MockServer::start().await; + let input = serde_json::json!({"name": "udmin"}); + Mock::given(method("POST")).and(path("/echo")) + .respond_with(|req: &wiremock::Request| { + // Echo back the request body as JSON + let body = serde_json::from_slice::(&req.body).unwrap_or(Value::Null); + ResponseTemplate::new(201).set_body_json(body) + }) + .mount(&server) + .await; + + let req = HttpRequest { + method: "POST".into(), + url: format!("{}/echo", server.uri()), + body: Some(input.clone()), + ..Default::default() + }; + let opts = HttpClientOptions::default(); + let resp = execute_http(req, opts).await.unwrap(); + assert_eq!(resp.status, 201); + assert_eq!(resp.body, input); + } + + #[tokio::test] + async fn test_timeout() { + let server = MockServer::start().await; + // Delay longer than our timeout + Mock::given(method("GET")) + .and(path("/slow")) + .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_millis(200))) + .mount(&server) + .await; + + let req = HttpRequest { method: "GET".into(), url: format!("{}/slow", server.uri()), ..Default::default() }; + let opts = HttpClientOptions { timeout_ms: Some(50), ..Default::default() }; + let err = execute_http(req, opts).await.unwrap_err(); + // Try to verify it's a timeout error from reqwest + let is_timeout = err + .downcast_ref::() + .map(|e| e.is_timeout()) + .unwrap_or(false); + assert!(is_timeout, "expected timeout error, got: {err}"); + } +} \ No newline at end of file diff --git a/backend/src/middlewares/mod.rs b/backend/src/middlewares/mod.rs index 4c04569..bbe9321 100644 --- a/backend/src/middlewares/mod.rs +++ b/backend/src/middlewares/mod.rs @@ -1,2 +1,4 @@ pub mod jwt; -pub mod logging; \ No newline at end of file +pub mod logging; +pub mod sse; +pub mod http_client; \ No newline at end of file diff --git a/backend/src/middlewares/sse.rs b/backend/src/middlewares/sse.rs new file mode 100644 index 0000000..e690715 --- /dev/null +++ b/backend/src/middlewares/sse.rs @@ -0,0 +1,75 @@ +use axum::response::sse::{Event, KeepAlive, Sse}; +use futures::Stream; +use std::convert::Infallible; +use std::time::Duration; +use tokio_stream::{wrappers::ReceiverStream, StreamExt as _}; + +// 引入后端流式事件类型 +use crate::flow::context::StreamEvent; + +// 新增:日志与时间戳 +use tracing::info; +use chrono::Utc; + +/// 将 mpsc::Receiver 包装为 SSE 响应,其中 T 需实现 Serialize +/// - 自动序列化为 JSON 文本并写入 data: 行 +/// - 附带 keep-alive,避免长连接超时 +pub fn from_mpsc(rx: tokio::sync::mpsc::Receiver) -> Sse>> +where + T: serde::Serialize + Send + 'static, +{ + let stream = ReceiverStream::new(rx).map(|evt| { + let payload = serde_json::to_string(&evt).unwrap_or_else(|_| "{}".to_string()); + // 关键日志:每次将事件映射为 SSE 帧时记录时间点(代表即将写入响应流) + info!(target: "udmin.sse", ts = %Utc::now().to_rfc3339(), payload_len = payload.len(), "sse send"); + Ok::(Event::default().data(payload)) + }); + + Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(10)).text("keep-alive")) +} + +/// 统一发送:节点事件 +pub async fn emit_node( + tx: &tokio::sync::mpsc::Sender, + node_id: impl Into, + logs: Vec, + ctx: serde_json::Value, +) { + let nid = node_id.into(); + // 日志:事件入队时间 + info!(target: "udmin.sse", kind = "node", node_id = %nid, logs_len = logs.len(), ts = %Utc::now().to_rfc3339(), "enqueue event"); + let _ = tx + .send(StreamEvent::Node { + node_id: nid, + logs, + ctx, + }) + .await; +} + +/// 统一发送:完成事件 +pub async fn emit_done( + tx: &tokio::sync::mpsc::Sender, + ok: bool, + ctx: serde_json::Value, + logs: Vec, +) { + info!(target: "udmin.sse", kind = "done", ok = ok, logs_len = logs.len(), ts = %Utc::now().to_rfc3339(), "enqueue event"); + let _ = tx + .send(StreamEvent::Done { ok, ctx, logs }) + .await; +} + +/// 统一发送:错误事件 +pub async fn emit_error( + tx: &tokio::sync::mpsc::Sender, + message: impl Into, +) { + let msg = message.into(); + info!(target: "udmin.sse", kind = "error", message = %msg, ts = %Utc::now().to_rfc3339(), "enqueue event"); + let _ = tx + .send(StreamEvent::Error { + message: msg, + }) + .await; +} \ No newline at end of file diff --git a/backend/src/routes/flows.rs b/backend/src/routes/flows.rs index ffa01da..e0a965a 100644 --- a/backend/src/routes/flows.rs +++ b/backend/src/routes/flows.rs @@ -4,11 +4,16 @@ use serde::Deserialize; use tracing::{info, error}; use crate::middlewares::jwt::AuthUser; +// 新增:引入通用 SSE 组件 +use crate::middlewares::sse; + pub fn router() -> Router { Router::new() .route("/flows", post(create).get(list)) .route("/flows/{id}", get(get_one).put(update).delete(remove)) .route("/flows/{id}/run", post(run)) + // 新增:流式运行(SSE)端点 + .route("/flows/{id}/run/stream", post(run_stream)) } #[derive(Deserialize)] @@ -82,4 +87,23 @@ async fn run(State(db): State, user: AuthUser, Path(id): Path, Json( Err(AppError::InternalMsg(full)) } } +} + +// 新增:SSE 流式运行端点,请求体沿用 RunReq(只包含 input) +async fn run_stream(State(db): State, user: AuthUser, Path(id): Path, Json(req): Json) -> Result>>, AppError> { + // 建立 mpsc 通道用于接收引擎的流式事件 + let (tx, rx) = tokio::sync::mpsc::channel::(16); + + // 启动后台任务运行流程,将事件通过 tx 发送 + let db_clone = db.clone(); + let id_clone = id.clone(); + let input = req.input.clone(); + let user_info = Some((user.uid, user.username)); + tokio::spawn(async move { + // 复用 flow_service::run 内部大部分逻辑,但通过 DriveOptions 注入 event_tx + let _ = flow_service::run_with_stream(db_clone, &id_clone, flow_service::RunReq { input }, user_info, tx).await; + }); + + // 由通用组件把 Receiver 包装为 SSE 响应 + Ok(sse::from_mpsc(rx)) } \ No newline at end of file diff --git a/backend/src/services/flow_service.rs b/backend/src/services/flow_service.rs index e3739a3..2ce8582 100644 --- a/backend/src/services/flow_service.rs +++ b/backend/src/services/flow_service.rs @@ -4,7 +4,7 @@ use anyhow::Context as _; use serde::{Deserialize, Serialize}; use crate::error::AppError; -use crate::flow::{self, dsl::FlowDSL, engine::FlowEngine, context::{DriveOptions, ExecutionMode}}; +use crate::flow::{self, dsl::FlowDSL, engine::FlowEngine, context::{DriveOptions, ExecutionMode, StreamEvent}, log_handler::{FlowLogHandler, DatabaseLogHandler, SseLogHandler}}; use crate::db::Db; use crate::models::flow as db_flow; use crate::models::request_log; // 新增:查询最近修改人 @@ -14,6 +14,10 @@ use sea_orm::{EntityTrait, ActiveModelTrait, Set, DbErr, ColumnTrait, QueryFilte use sea_orm::entity::prelude::DateTimeWithTimeZone; // 新增:时间类型 use chrono::{Utc, FixedOffset}; use tracing::{info, error}; +// 新增:用于流式事件通道 +use tokio::sync::mpsc::Sender; +// 新增:用于错误下传递局部上下文与日志 +use crate::flow::engine::DriveError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FlowSummary { @@ -197,216 +201,200 @@ pub async fn delete(db: &Db, id: &str) -> anyhow::Result<()> { } pub async fn run(db: &Db, id: &str, req: RunReq, operator: Option<(i64, String)>) -> anyhow::Result { - info!(target = "udmin", "flow.run: start id={}", id); + let log_handler = DatabaseLogHandler::new(db.clone()); + match run_internal(db, id, req, operator, &log_handler, None).await { + Ok((ctx, logs)) => Ok(RunResult { ok: true, ctx, logs }), + Err(e) => { + // 将运行期错误转换为 ok=false,并尽量带上部分 ctx/logs + if let Some(de) = e.downcast_ref::().cloned() { + Ok(RunResult { ok: false, ctx: de.ctx, logs: de.logs }) + } else { + let mut full = e.to_string(); + for cause in e.chain().skip(1) { + full.push_str(" | "); + full.push_str(&cause.to_string()); + } + Ok(RunResult { ok: false, ctx: serde_json::json!({}), logs: vec![full] }) + } + } + } +} + +// 新增:流式运行,向外发送节点事件与最终完成事件 +pub async fn run_with_stream( + db: Db, + id: &str, + req: RunReq, + operator: Option<(i64, String)>, + event_tx: Sender, +) -> anyhow::Result<()> { + // clone 一份用于错误时补发 done + let tx_done = event_tx.clone(); + let log_handler = SseLogHandler::new(db.clone(), event_tx.clone()); + match run_internal(&db, id, req, operator, &log_handler, Some(event_tx)).await { + Ok((_ctx, _logs)) => Ok(()), // 正常路径:log_success 内已发送 done(true,...) + Err(e) => { + // 错误路径:先在 log_error 中已发送 error 事件;此处补发 done(false,...) + if let Some(de) = e.downcast_ref::().cloned() { + crate::middlewares::sse::emit_done(&tx_done, false, de.ctx, de.logs).await; + } else { + let mut full = e.to_string(); + for cause in e.chain().skip(1) { full.push_str(" | "); full.push_str(&cause.to_string()); } + crate::middlewares::sse::emit_done(&tx_done, false, serde_json::json!({}), vec![full]).await; + } + Ok(()) + } + } +} + +// 内部统一的运行方法 +async fn run_internal( + db: &Db, + id: &str, + req: RunReq, + operator: Option<(i64, String)>, + log_handler: &dyn FlowLogHandler, + event_tx: Option>, +) -> anyhow::Result<(serde_json::Value, Vec)> { + // 使用传入的 event_tx(当启用 SSE 时由路由层提供) + info!(target = "udmin", "flow.run_internal: start id={}", id); let start = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()); - // 获取流程编码,便于写入运行日志 + + // 获取流程编码 let flow_code: Option = match db_flow::Entity::find_by_id(id.to_string()).one(db).await { Ok(Some(row)) => row.code, _ => None, }; -// 获取流程文档并记录失败原因 + + // 获取流程文档 let doc = match get(db, id).await { Ok(d) => d, Err(e) => { - error!(target = "udmin", error = ?e, "flow.run: get doc failed id={}", id); - // 记录失败日志 - let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None)); - let _ = flow_run_log_service::create(db, CreateRunLogInput { - flow_id: id.to_string(), - flow_code: flow_code.clone(), - input: Some(serde_json::to_string(&req.input).unwrap_or_default()), - output: None, - ok: false, - logs: Some(format!("get doc failed: {}", e)), - user_id, - username, - started_at: start, - duration_ms: 0, - }).await; + error!(target = "udmin", error = ?e, "flow.run_internal: get doc failed id={}", id); + let error_msg = format!("get doc failed: {}", e); + log_handler.log_error(id, flow_code.as_deref(), &req.input, &error_msg, operator, start, 0).await?; return Err(e); } }; - // 记录文档基本信息,便于判断走 JSON 还是 YAML - info!(target = "udmin", "flow.run: doc loaded id={} has_design_json={} yaml_len={}", id, doc.design_json.is_some(), doc.yaml.len()); + info!(target = "udmin", "flow.run_internal: doc loaded id={} has_design_json={} yaml_len={}", id, doc.design_json.is_some(), doc.yaml.len()); - // Prefer design_json if present; otherwise fall back to YAML + // 构建 chain 与 ctx let mut exec_mode: ExecutionMode = ExecutionMode::Sync; let (mut chain, mut ctx) = if let Some(design) = &doc.design_json { - info!(target = "udmin", "flow.run: building chain from design_json id={}", id); - let chain_from_json = match flow::dsl::chain_from_design_json(design) { - Ok(c) => c, - Err(e) => { - error!(target = "udmin", error = ?e, "flow.run: build chain from design_json failed id={}", id); - // 记录失败日志 - let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None)); - let _ = flow_run_log_service::create(db, CreateRunLogInput { - flow_id: id.to_string(), - flow_code: flow_code.clone(), - input: Some(serde_json::to_string(&req.input).unwrap_or_default()), - output: None, - ok: false, - logs: Some(format!("build chain from design_json failed: {}", e)), - user_id, - username, - started_at: start, - duration_ms: 0, - }).await; - return Err(e); - } - }; - let mut ctx = req.input.clone(); - // Merge node-scoped configs into ctx under ctx.nodes - let supplement = flow::mappers::ctx_from_design_json(design); - merge_json(&mut ctx, &supplement); - // 解析 executionMode / execution_mode - let mode_str = design.get("executionMode").and_then(|v| v.as_str()) - .or_else(|| design.get("execution_mode").and_then(|v| v.as_str())) - .unwrap_or("sync"); - exec_mode = parse_execution_mode(mode_str); - info!(target = "udmin", "flow.run: ctx prepared from design_json id={} execution_mode={:?}", id, exec_mode); - (chain_from_json, ctx) - } else { - info!(target = "udmin", "flow.run: parsing YAML id={}", id); - let dsl = match serde_yaml::from_str::(&doc.yaml) { - Ok(d) => d, - Err(e) => { - error!(target = "udmin", error = ?e, "flow.run: parse YAML failed id={}", id); - // 记录失败日志 - let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None)); - let _ = flow_run_log_service::create(db, CreateRunLogInput { - flow_id: id.to_string(), - flow_code: flow_code.clone(), - input: Some(serde_json::to_string(&req.input).unwrap_or_default()), - output: None, - ok: false, - logs: Some(format!("parse YAML failed: {}", e)), - user_id, - username, - started_at: start, - duration_ms: 0, - }).await; - return Err(anyhow::Error::new(e).context("invalid flow yaml")); - } - }; - // 从 YAML 读取执行模式 - if let Some(m) = dsl.execution_mode.as_deref() { exec_mode = parse_execution_mode(m); } - (dsl.into(), req.input.clone()) - }; + let chain_from_json = match flow::dsl::chain_from_design_json(design) { + Ok(c) => c, + Err(e) => { + error!(target = "udmin", error = ?e, "flow.run_internal: build chain from design_json failed id={}", id); + let error_msg = format!("build chain from design_json failed: {}", e); + log_handler.log_error(id, flow_code.as_deref(), &req.input, &error_msg, operator, start, 0).await?; + return Err(e); + } + }; + let mut ctx = req.input.clone(); + let supplement = flow::mappers::ctx_from_design_json(design); + merge_json(&mut ctx, &supplement); + let mode_str = design.get("executionMode").and_then(|v| v.as_str()) + .or_else(|| design.get("execution_mode").and_then(|v| v.as_str())) + .unwrap_or("sync"); + exec_mode = parse_execution_mode(mode_str); + (chain_from_json, ctx) + } else { + let dsl = match serde_yaml::from_str::(&doc.yaml) { + Ok(d) => d, + Err(e) => { + error!(target = "udmin", error = ?e, "flow.run_internal: parse YAML failed id={}", id); + let error_msg = format!("parse YAML failed: {}", e); + log_handler.log_error(id, flow_code.as_deref(), &req.input, &error_msg, operator, start, 0).await?; + return Err(anyhow::Error::new(e).context("invalid flow yaml")); + } + }; + if let Some(m) = dsl.execution_mode.as_deref() { exec_mode = parse_execution_mode(m); } + (dsl.into(), req.input.clone()) + }; - // 若 design_json 解析出的 chain 为空,兜底回退到 YAML + // 兜底回退 if chain.nodes.is_empty() { - info!(target = "udmin", "flow.run: empty chain from design_json, fallback to YAML id={}", id); if !doc.yaml.trim().is_empty() { match serde_yaml::from_str::(&doc.yaml) { Ok(dsl) => { chain = dsl.clone().into(); - // YAML 分支下 ctx = req.input,不再追加 design_json 的补充 ctx = req.input.clone(); if let Some(m) = dsl.execution_mode.as_deref() { exec_mode = parse_execution_mode(m); } - info!(target = "udmin", "flow.run: fallback YAML parsed id={} execution_mode={:?}", id, exec_mode); } Err(e) => { - error!(target = "udmin", error = ?e, "flow.run: fallback parse YAML failed id={}", id); - // 保留原空 chain,稍后 drive 会再次报错,但这里先返回更明确的错误 - let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None)); - let _ = flow_run_log_service::create(db, CreateRunLogInput { - flow_id: id.to_string(), - flow_code: flow_code.clone(), - input: Some(serde_json::to_string(&req.input).unwrap_or_default()), - output: None, - ok: false, - logs: Some(format!("fallback parse YAML failed: {}", e)), - user_id, - username, - started_at: start, - duration_ms: 0, - }).await; + let error_msg = format!("fallback parse YAML failed: {}", e); + log_handler.log_error(id, flow_code.as_deref(), &req.input, &error_msg, operator, start, 0).await?; return Err(anyhow::anyhow!("empty chain: design_json produced no nodes and YAML parse failed")); } } } else { - // YAML 也为空 - let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None)); - let _ = flow_run_log_service::create(db, CreateRunLogInput { - flow_id: id.to_string(), - flow_code: flow_code.clone(), - input: Some(serde_json::to_string(&req.input).unwrap_or_default()), - output: None, - ok: false, - logs: Some("empty chain: both design_json and yaml are empty".to_string()), - user_id, - username, - started_at: start, - duration_ms: 0, - }).await; - return Err(anyhow::anyhow!("empty chain: both design_json and yaml are empty")); + let error_msg = "empty chain: both design_json and yaml are empty"; + log_handler.log_error(id, flow_code.as_deref(), &req.input, error_msg, operator, start, 0).await?; + return Err(anyhow::anyhow!(error_msg)); } } - // 从全局注册中心获取任务(若未初始化则返回默认注册表) + // 任务与引擎 let tasks: flow::task::TaskRegistry = flow::task::get_registry(); let engine = FlowEngine::builder().tasks(tasks).build(); - info!(target = "udmin", "flow.run: driving engine id={} nodes={} links={} execution_mode={:?}", id, chain.nodes.len(), chain.links.len(), exec_mode); // 执行 let drive_res = engine - .drive(&chain, ctx, DriveOptions { execution_mode: exec_mode.clone(), ..Default::default() }) + .drive(&chain, ctx, DriveOptions { execution_mode: exec_mode.clone(), event_tx, ..Default::default() }) .await; - let (ctx, logs) = match drive_res { - Ok(r) => r, - Err(e) => { - error!(target = "udmin", error = ?e, "flow.run: engine drive failed id={}", id); - let dur = (Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()) - start).num_milliseconds() as i64; - let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None)); - let _ = flow_run_log_service::create(db, CreateRunLogInput { - flow_id: id.to_string(), - flow_code: flow_code.clone(), - input: Some(serde_json::to_string(&req.input).unwrap_or_default()), - output: None, - ok: false, - logs: Some(format!("engine drive failed: {}", e)), - user_id, - username, - started_at: start, - duration_ms: dur, - }).await; - return Err(e); - } - }; - // 兜底移除 variable 节点:不在最终上下文暴露 variable_* 的配置 - let mut ctx = ctx; - if let serde_json::Value::Object(map) = &mut ctx { - if let Some(serde_json::Value::Object(nodes)) = map.get_mut("nodes") { - let keys: Vec = nodes - .iter() - .filter_map(|(k, v)| if v.get("variable").is_some() { Some(k.clone()) } else { None }) - .collect(); - for k in keys { nodes.remove(&k); } + match drive_res { + Ok((mut ctx, logs)) => { + // 移除 variable 节点 + if let serde_json::Value::Object(map) = &mut ctx { + if let Some(serde_json::Value::Object(nodes)) = map.get_mut("nodes") { + let keys: Vec = nodes + .iter() + .filter_map(|(k, v)| if v.get("variable").is_some() { Some(k.clone()) } else { None }) + .collect(); + for k in keys { nodes.remove(&k); } + } + } + let dur = (Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()) - start).num_milliseconds() as i64; + log_handler.log_success(id, flow_code.as_deref(), &req.input, &ctx, &logs, operator, start, dur).await?; + Ok((ctx, logs)) + } + Err(e) => { + error!(target = "udmin", error = ?e, "flow.run_internal: engine drive failed id={}", id); + let dur = (Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()) - start).num_milliseconds() as i64; + // 优先记录详细错误(包含部分 ctx 与累计 logs) + if let Some(de) = e.downcast_ref::().cloned() { + log_handler + .log_error_detail( + id, + flow_code.as_deref(), + &req.input, + &de.ctx, + &de.logs, + &de.message, + operator, + start, + dur, + ) + .await?; + } else { + let error_msg = format!("engine drive failed: {}", e); + log_handler + .log_error( + id, + flow_code.as_deref(), + &req.input, + &error_msg, + operator, + start, + dur, + ) + .await?; + } + Err(e) } } - - // 调试:打印处理后的 ctx - //info!(target = "udmin", "flow.run: result ctx={}", serde_json::to_string(&ctx).unwrap_or_else(|_| "".to_string())); - - info!(target = "udmin", "flow.run: done id={}", id); - // 写入成功日志 - let dur = (Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()) - start).num_milliseconds() as i64; - let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None)); - let _ = flow_run_log_service::create(db, CreateRunLogInput { - flow_id: id.to_string(), - flow_code: flow_code.clone(), - input: Some(serde_json::to_string(&req.input).unwrap_or_default()), - output: Some(serde_json::to_string(&ctx).unwrap_or_default()), - ok: true, - logs: Some(serde_json::to_string(&logs).unwrap_or_default()), - user_id, - username, - started_at: start, - duration_ms: dur, - }).await; - Ok(RunResult { ok: true, ctx, logs }) } fn extract_name(yaml: &str) -> Option { diff --git a/backend/src/utils/password.rs b/backend/src/utils/password.rs index 6dc0490..479f3f3 100644 --- a/backend/src/utils/password.rs +++ b/backend/src/utils/password.rs @@ -1,6 +1,6 @@ pub fn hash_password(plain: &str) -> anyhow::Result { - use argon2::{password_hash::{SaltString, PasswordHasher}, Argon2}; - let salt = SaltString::generate(&mut rand::thread_rng()); + use argon2::{password_hash::{SaltString, PasswordHasher, rand_core::OsRng}, Argon2}; + let salt = SaltString::generate(&mut OsRng); let hashed = Argon2::default() .hash_password(plain.as_bytes(), &salt) .map_err(|e| anyhow::anyhow!(e.to_string()))? diff --git a/cookies_admin.txt b/cookies_admin.txt index c31d989..f7ea1b9 100644 --- a/cookies_admin.txt +++ b/cookies_admin.txt @@ -2,3 +2,4 @@ # https://curl.se/docs/http-cookies.html # This file was generated by libcurl! Edit at your own risk. +#HttpOnly_127.0.0.1 FALSE / FALSE 1759594896 refresh_token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVpZCI6MSwiaXNzIjoidWRtaW4iLCJleHAiOjE3NTk1OTQ4OTYsInR5cCI6InJlZnJlc2gifQ.zH6gGProbzh4U7RzgYNH4DqD2-EyzvotbkGUfMBzp4k diff --git a/frontend/src/flows/components/testrun/testrun-panel/index.tsx b/frontend/src/flows/components/testrun/testrun-panel/index.tsx index 463a85e..30a0fe7 100644 --- a/frontend/src/flows/components/testrun/testrun-panel/index.tsx +++ b/frontend/src/flows/components/testrun/testrun-panel/index.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MIT */ -import { FC, useContext, useEffect, useState } from 'react'; +import { FC, useContext, useEffect, useRef, useState } from 'react'; import classnames from 'classnames'; import { useService, I18n } from '@flowgram.ai/free-layout-editor'; @@ -41,6 +41,21 @@ export const TestRunSidePanel: FC = ({ visible, onCancel | undefined >(); + // 模式切换:SSE 流式 or 普通 HTTP + const [streamMode, _setStreamMode] = useState(() => { + const saved = localStorage.getItem('testrun-stream-mode'); + return saved ? JSON.parse(saved) : true; + }); + const setStreamMode = (checked: boolean) => { + _setStreamMode(checked); + localStorage.setItem('testrun-stream-mode', JSON.stringify(checked)); + }; + + // 流式渲染:实时上下文与日志 + const [streamCtx, setStreamCtx] = useState(); + const [streamLogs, setStreamLogs] = useState([]); + const cancelRef = useRef<(() => void) | null>(null); + // en - Use localStorage to persist the JSON mode state const [inputJSONMode, _setInputJSONMode] = useState(() => { const savedMode = localStorage.getItem('testrun-input-json-mode'); @@ -63,11 +78,13 @@ export const TestRunSidePanel: FC = ({ visible, onCancel const onTestRun = async () => { if (isRunning) { - // 后端运行不可取消,这里直接忽略重复点击 + // 运行中,忽略重复点击 return; } setResult(undefined); setErrors(undefined); + setStreamCtx(undefined); + setStreamLogs([]); setRunning(true); try { // 运行前保存(静默),确保后端 YAML 与编辑器一致;若保存失败则不继续运行 @@ -76,29 +93,75 @@ export const TestRunSidePanel: FC = ({ visible, onCancel setErrors([I18n.t('Save failed, cannot run')]); return; } - const runRes = await customService.run(values); - if (runRes) { - // 若后端返回 ok=false,则视为失败并展示失败信息与日志 - if ((runRes as any).ok === false) { - setResult(runRes as any); - const err = extractErrorMsg((runRes as any).logs) || I18n.t('Run failed'); - setErrors([err]); + + if (streamMode) { + const { cancel, done } = customService.runStream(values, { + onNode: (evt) => { + if (evt.ctx) setStreamCtx((prev: any) => ({ ...(prev || {}), ...(evt.ctx || {}) })); + if (evt.logs && evt.logs.length) setStreamLogs((prev: string[]) => [...prev, ...evt.logs!]); + }, + onError: (evt) => { + const msg = evt.message || I18n.t('Run failed'); + setErrors((prev) => [...(prev || []), msg]); + }, + onDone: (evt) => { + setResult({ ok: evt.ok, ctx: evt.ctx, logs: evt.logs }); + }, + onFatal: (err) => { + setErrors((prev) => [...(prev || []), err.message || String(err)]); + setRunning(false); + }, + }); + + cancelRef.current = cancel; + + const finished = await done; + if (finished) { + setResult(finished as any); } else { - setResult(runRes as any); + // 流结束但未收到 done 事件,给出提示 + setErrors((prev) => [...(prev || []), I18n.t('Stream terminated without completion')]); } } else { - setErrors([I18n.t('Run failed')]); + // 普通 HTTP 一次性运行 + try { + const runRes = await customService.run(values); + if (runRes) { + if ((runRes as any).ok === false) { + setResult(runRes as any); + const err = extractErrorMsg((runRes as any).logs) || I18n.t('Run failed'); + setErrors([err]); + } else { + setResult(runRes as any); + } + } else { + setErrors([I18n.t('Run failed')]); + } + } catch (e: any) { + setErrors([e?.message || I18n.t('Run failed')]); + } } } catch (e: any) { setErrors([e?.message || I18n.t('Run failed')]); } finally { setRunning(false); + cancelRef.current = null; } }; + const onCancelRun = () => { + try { cancelRef.current?.(); } catch {} + setRunning(false); + }; + const onClose = async () => { setValues({}); + if (isRunning) { + if (streamMode) onCancelRun(); + } setRunning(false); + setStreamCtx(undefined); + setStreamLogs([]); onCancel(); }; @@ -119,6 +182,20 @@ export const TestRunSidePanel: FC = ({ visible, onCancel
{I18n.t('Running...')}
+ {/* 实时输出(仅流式模式显示) */} + {streamMode && ( + <> + {errors?.length ? ( +
+ {errors.map((e) => ( +
{e}
+ ))} +
+ ) : null} + + + + )}
); @@ -141,6 +218,12 @@ export const TestRunSidePanel: FC = ({ visible, onCancel onChange={(checked: boolean) => setInputJSONMode(checked)} size="small" /> +
{I18n.t('Streaming Mode')}
+ setStreamMode(checked)} + size="small" + /> {renderStatus} {errors?.map((e) => ( @@ -153,6 +236,13 @@ export const TestRunSidePanel: FC = ({ visible, onCancel ) : ( )} + {/* 运行中(流式)时,直接在表单区域下方展示实时输出,而不是覆盖整块内容 */} + {streamMode && isRunning && ( + <> + + + + )} {/* 展示后端返回的执行信息 */} @@ -161,8 +251,12 @@ export const TestRunSidePanel: FC = ({ visible, onCancel const renderButton = (