diff --git a/CHANGELOG.md b/CHANGELOG.md index d9475b629..626c5801d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to OpenFang will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.12] - 2026-03-04 + +### Added + +#### Feishu Integration +- **WebSocket long connection support** for Feishu/Lark messaging platform + - Enables local development without public IP or port forwarding + - Custom protobuf protocol implementation with manual encoding/decoding + - Automatic ping/pong keepalive mechanism (configurable interval) + - Message fragmentation handling for large payloads + - Gzip compression support for message payloads + - Dynamic WebSocket URL fetching via HTTP config endpoint +- **Connection mode configuration**: Choose between webhook (HTTP) and WebSocket modes +- **Enhanced logging**: Improved visibility for WebSocket connection status and message flow + +### Changed + +#### Feishu Integration +- `FeishuAdapter::new()`: Now requires `connection_mode` parameter instead of `webhook_port` +- Fixed `receive_id` error by using `chat_id` instead of `sender_id` for user platform_id +- Upgraded log levels from `trace!` to `info!` for better production visibility + +### Dependencies + +- Added `tokio-tungstenite` for WebSocket client functionality +- Added `prost` and `prost-types` for protobuf encoding +- Added `flate2` for gzip decompression +- Updated `Cargo.lock` with new dependency versions + ## [0.1.0] - 2026-02-24 ### Added diff --git a/Cargo.lock b/Cargo.lock index 88111003a..1cccc7c8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,9 +221,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", @@ -258,7 +258,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -289,7 +289,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -315,7 +315,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -735,9 +735,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -931,9 +931,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", "core-foundation", @@ -973,36 +973,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0377b13bf002a0774fcccac4f1102a10f04893d24060cf4b7350c87e4cbb647c" +checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa027979140d023b25bf7509fb7ede3a54c3d3871fb5ead4673c4b633f671a2" +checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618e4da87d9179a70b3c2f664451ca8898987aa6eb9f487d16988588b5d8cc40" +checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db53764b5dad233b37b8f5dc54d3caa9900c54579195e00f17ea21f03f71aaa7" +checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" dependencies = [ "serde", "serde_derive", @@ -1010,9 +1010,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae927f1d8c0abddaa863acd201471d56e7fc6c3925104f4861ed4dc3e28b421" +checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1037,9 +1037,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fcf1e3e6757834bd2584f4cbff023fcc198e9279dcb5d684b4bb27a9b19f54" +checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1050,24 +1050,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "205dcb9e6ccf9d368b7466be675ff6ee54a63e36da6fe20e72d45169cf6fd254" +checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" [[package]] name = "cranelift-control" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "108eca9fcfe86026054f931eceaf57b722c1b97464bf8265323a9b5877238817" +checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d96496910065d3165f84ff8e1e393916f4c086f88ac8e1b407678bc78735aa" +checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" dependencies = [ "cranelift-bitset", "serde", @@ -1076,9 +1076,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e303983ad7e23c850f24d9c41fc3cb346e1b930f066d3966545e4c98dac5c9fb" +checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111" dependencies = [ "cranelift-codegen", "log", @@ -1088,15 +1088,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b0cf8d867d891245836cac7abafb0a5b0ea040a019d720702b3b8bcba40bfa" +checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa" [[package]] name = "cranelift-native" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24b641e315443e27807b69c440fe766737d7e718c68beb665a2d69259c77bf3" +checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e" dependencies = [ "cranelift-codegen", "libc", @@ -1105,9 +1105,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e378a54e7168a689486d67ee1f818b7e5356e54ae51a1d7a53f4f13f7f8b7a" +checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" [[package]] name = "crc32fast" @@ -1509,17 +1509,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", "block2", @@ -1720,9 +1714,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -2154,7 +2148,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-link 0.2.1", ] @@ -2191,20 +2185,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -2890,9 +2884,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -2929,6 +2923,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -3030,9 +3033,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.87" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -3196,13 +3199,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags 2.11.0", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -3211,7 +3215,6 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" dependencies = [ - "cc", "pkg-config", "vcpkg", ] @@ -3224,9 +3227,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -3360,7 +3363,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -3390,9 +3393,9 @@ dependencies = [ [[package]] name = "minisign-verify" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" [[package]] name = "miniz_oxide" @@ -3592,7 +3595,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -3600,9 +3603,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -3616,38 +3619,8 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.0", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.11.0", - "objc2", "objc2-foundation", ] @@ -3675,41 +3648,6 @@ dependencies = [ "objc2-io-surface", ] -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -3749,16 +3687,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-osa-kit" version = "0.3.2" @@ -3783,17 +3711,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "objc2-security" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-ui-kit" version = "0.3.2" @@ -3818,8 +3735,6 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", - "objc2-javascript-core", - "objc2-security", ] [[package]] @@ -3866,7 +3781,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "axum", @@ -3903,13 +3818,15 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "axum", "base64 0.22.1", + "bytes", "chrono", "dashmap", + "flate2", "futures", "hex", "hmac", @@ -3918,6 +3835,8 @@ dependencies = [ "mailparse", "native-tls", "openfang-types", + "prost 0.13.5", + "prost-types", "reqwest 0.12.28", "serde", "serde_json", @@ -3934,7 +3853,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.3.19" +version = "0.3.20" dependencies = [ "clap", "clap_complete", @@ -3961,7 +3880,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.3.19" +version = "0.3.20" dependencies = [ "axum", "open", @@ -3987,7 +3906,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.3.19" +version = "0.3.20" dependencies = [ "aes-gcm", "argon2", @@ -4015,7 +3934,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.3.19" +version = "0.3.20" dependencies = [ "chrono", "dashmap", @@ -4032,7 +3951,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "chrono", @@ -4068,7 +3987,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "chrono", @@ -4087,7 +4006,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.3.19" +version = "0.3.20" dependencies = [ "chrono", "dirs 6.0.0", @@ -4106,7 +4025,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.3.19" +version = "0.3.20" dependencies = [ "anyhow", "async-trait", @@ -4138,7 +4057,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.3.19" +version = "0.3.20" dependencies = [ "chrono", "hex", @@ -4161,7 +4080,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "chrono", @@ -4180,7 +4099,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.3.19" +version = "0.3.20" dependencies = [ "async-trait", "chrono", @@ -4555,9 +4474,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -4567,9 +4486,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -4592,6 +4511,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -4641,7 +4566,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4737,11 +4662,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.4+spec-1.1.0", ] [[package]] @@ -4783,6 +4708,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost 0.12.6", +] + [[package]] name = "psm" version = "0.1.30" @@ -4795,9 +4775,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01051a5b172e07f9197b85060e6583b942aec679dac08416647bf7e7dc916b65" +checksum = "e9812652c1feb63cf39f8780cecac154a32b22b3665806c733cd4072547233a4" dependencies = [ "cranelift-bitset", "log", @@ -4807,9 +4787,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf194f5b1a415ef3a44ee35056f4009092cc4038a9f7e3c7c1e392f48ee7dbb" +checksum = "56000349b6896e3d44286eb9c330891237f40b27fd43c1ccc84547d0b463cb40" dependencies = [ "proc-macro2", "quote", @@ -4818,12 +4798,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "quanta" @@ -4915,9 +4892,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4934,6 +4911,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -5120,9 +5103,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags 2.11.0", ] @@ -5214,9 +5197,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -5408,22 +5391,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -5752,9 +5735,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", "chrono", @@ -5771,9 +5754,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -6207,23 +6190,22 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.34.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" dependencies = [ "bitflags 2.11.0", "block2", "core-foundation", "core-graphics", "crossbeam-channel", - "dispatch", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", @@ -6235,7 +6217,6 @@ dependencies = [ "once_cell", "parking_lot", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -6281,9 +6262,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tauri" -version = "2.10.2" +version = "2.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" dependencies = [ "anyhow", "bytes", @@ -6333,9 +6314,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.5" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" dependencies = [ "anyhow", "cargo_toml", @@ -6355,9 +6336,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.5.4" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" dependencies = [ "base64 0.22.1", "brotli", @@ -6382,9 +6363,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.4" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -6396,9 +6377,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" dependencies = [ "anyhow", "glob", @@ -6570,9 +6551,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" dependencies = [ "cookie", "dpi", @@ -6595,9 +6576,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", "http", @@ -6605,7 +6586,6 @@ dependencies = [ "log", "objc2", "objc2-app-kit", - "objc2-foundation", "once_cell", "percent-encoding", "raw-window-handle", @@ -6622,9 +6602,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" dependencies = [ "anyhow", "brotli", @@ -6683,14 +6663,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -6821,9 +6801,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -6838,9 +6818,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -6965,6 +6945,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -6991,12 +6980,12 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "winnow 0.7.14", ] @@ -7227,13 +7216,13 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -7395,7 +7384,7 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -7496,9 +7485,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.110" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -7509,9 +7498,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.60" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42e96ea38f49b191e08a1bab66c7ffdba24b06f9995b39a9dd60222e5b6f1da" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -7523,9 +7512,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.110" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7533,9 +7522,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.110" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -7546,9 +7535,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.110" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -7691,9 +7680,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19f56cece843fa95dd929f5568ff8739c7e3873b530ceea9eda2aa02a0b4142" +checksum = "e2a83182bf04af87571b4c642300479501684f26bab5597f68f68cded5b098fd" dependencies = [ "addr2line", "anyhow", @@ -7718,7 +7707,7 @@ dependencies = [ "postcard", "pulley-interpreter", "rayon", - "rustix 1.1.3", + "rustix 1.1.4", "semver", "serde", "serde_derive", @@ -7748,9 +7737,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf9dff572c950258548cbbaf39033f68f8dcd0b43b22e80def9fe12d532d3e5" +checksum = "cb201c41aa23a3642365cfb2e4a183573d85127a3c9d528f56b9997c984541ab" dependencies = [ "anyhow", "cpp_demangle", @@ -7775,15 +7764,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f52a985f5b5dae53147fc596f3a313c334e2c24fd1ba708634e1382f6ecd727" +checksum = "fb5b3069d1a67ba5969d0eb1ccd7e141367d4e713f4649aa90356c98e8f19bea" dependencies = [ "base64 0.22.1", "directories-next", "log", "postcard", - "rustix 1.1.3", + "rustix 1.1.4", "serde", "serde_derive", "sha2", @@ -7795,9 +7784,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7920dc7dcb608352f5fe93c52582e65075b7643efc5dac3fc717c1645a8d29a0" +checksum = "0c924400db7b6ca996fef1b23beb0f41d5c809836b1ec60fc25b4057e2d25d9b" dependencies = [ "anyhow", "proc-macro2", @@ -7810,15 +7799,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066f5aed35aa60580a2ac0df145c0f0d4b04319862fee1d6036693e1cca43a12" +checksum = "7d3f65daf4bf3d74ca2fbbe20af0589c42e2b398a073486451425d94fd4afef4" [[package]] name = "wasmtime-internal-cranelift" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb8002dc415b7773d7949ee360c05ee8f91627ec25a7a0b01ee03831bdfdda1" +checksum = "633e889cdae76829738db0114ab3b02fce51ea4a1cd9675a67a65fce92e8b418" dependencies = [ "cfg-if", "cranelift-codegen", @@ -7843,14 +7832,14 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9c562c5a272bc9f615d8f0c085a4360bafa28eef9aa5947e63d204b1129b22" +checksum = "deb126adc5d0c72695cfb77260b357f1b81705a0f8fa30b3944e7c2219c17341" dependencies = [ "cc", "cfg-if", "libc", - "rustix 1.1.3", + "rustix 1.1.4", "wasmtime-environ", "wasmtime-internal-versioned-export-macros", "windows-sys 0.61.2", @@ -7858,21 +7847,21 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db673148f26e1211db3913c12c75594be9e3858a71fa297561e9162b1a49cfb0" +checksum = "8e66ff7f90a8002187691ff6237ffd09f954a0ebb9de8b2ff7f5c62632134120" dependencies = [ "cc", "object", - "rustix 1.1.3", + "rustix 1.1.4", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bada5ca1cc47df7d14100e2254e187c2486b426df813cea2dd2553a7469f7674" +checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08" dependencies = [ "anyhow", "cfg-if", @@ -7882,24 +7871,24 @@ dependencies = [ [[package]] name = "wasmtime-internal-math" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf6f615d528eda9adc6eefb062135f831b5215c348f4c3ec3e143690c730605b" +checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da169d4f789b586e1b2612ba8399c653ed5763edf3e678884ba785bb151d018f" +checksum = "9b63cbea1c0192c7feb7c0dfb35f47166988a3742f29f46b585ef57246c65764" [[package]] name = "wasmtime-internal-unwinder" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4888301f3393e4e8c75c938cce427293fade300fee3fc8fd466fdf3e54ae068e" +checksum = "f25c392c7e5fb891a7416e3c34cfbd148849271e8c58744fda875dde4bec4d6a" dependencies = [ "cfg-if", "cranelift-codegen", @@ -7910,9 +7899,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ba3124cc2cbcd362672f9f077303ccc4cd61daa908f73447b7fdaece75ff9f" +checksum = "70f8b9796a3f0451a7b702508b303d654de640271ac80287176de222f187a237" dependencies = [ "proc-macro2", "quote", @@ -7921,9 +7910,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a4182515dabba776656de4ebd62efad03399e261cf937ecccb838ce8823534" +checksum = "c0063e61f1d0b2c20e9cfc58361a6513d074a23c80b417aac3033724f51648a0" dependencies = [ "cranelift-codegen", "gimli", @@ -7938,9 +7927,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87acbd416227cdd279565ba49e57cf7f08d112657c3b3f39b70250acdfd094fe" +checksum = "587699ca7cae16b4a234ffcc834f37e75675933d533809919b52975f5609e2ef" dependencies = [ "anyhow", "bitflags 2.11.0", @@ -7973,9 +7962,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.87" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c7c5718134e770ee62af3b6b4a84518ec10101aad610c024b64d6ff29bb1ff" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -8122,9 +8111,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f31dcfdfaf9d6df9e1124d7c8ee6fc29af5b99b89d11ae731c138e0f5bd77b" +checksum = "c55de3ac5b8bd71e5f6c87a9e511dd3ceb194bdb58183c6a7bf21cd8c0e46fbc" dependencies = [ "anyhow", "cranelift-assembler-x64", @@ -8774,7 +8763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "gethostname", - "rustix 1.1.3", + "rustix 1.1.4", "x11rb-protocol", ] @@ -8791,7 +8780,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -8802,7 +8791,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.3.19" +version = "0.3.20" [[package]] name = "yoke" @@ -8849,7 +8838,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix 1.1.3", + "rustix 1.1.4", "serde", "serde_repr", "tracing", @@ -8868,7 +8857,7 @@ version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8890,18 +8879,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", @@ -9077,7 +9066,7 @@ version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", diff --git a/Cargo.toml b/Cargo.toml index eb6cc39f3..1bd65dc1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1", features = ["v4", "serde"] } # Database -rusqlite = { version = "0.31", features = ["bundled", "serde_json"] } +rusqlite = { version = "0.31", features = ["serde_json"] } # CLI clap = { version = "4", features = ["derive"] } @@ -79,6 +79,10 @@ futures = "0.3" tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "rustls-tls-native-roots"] } url = "2" +# Protocol Buffers (for Feishu WebSocket) +prost = "0.13" +prost-types = "0.12" + # WASM sandbox wasmtime = "41" @@ -134,6 +138,9 @@ imap = "2" native-tls = "0.2" mailparse = "0.15" +# Compression +flate2 = "1" + # Testing tokio-test = "0.4" tempfile = "3" diff --git a/FEISHU_WEBSOCKET_README.md b/FEISHU_WEBSOCKET_README.md new file mode 100644 index 000000000..d32a3a0ea --- /dev/null +++ b/FEISHU_WEBSOCKET_README.md @@ -0,0 +1,151 @@ +# Feishu WebSocket 长连接实现说明 + +## 实现概述 + +OpenFang 现已支持飞书 WebSocket 长连接模式,无需公网 IP 或内网穿透工具即可在本地环境中接收飞书事件。 + +## 配置方式 + +### 1. 配置文件 (`~/.openfang/config.toml`) + +```toml +[channels.feishu] +app_id = "cli_a922bea3c6785cc8" +app_secret_env = "FEISHU_APP_SECRET" +connection_mode = "websocket" # 改为 websocket +webhook_port = 8453 # WebSocket 模式下不需要 +default_agent = "assistant" +``` + +### 2. 支持的连接模式 + +| 模式 | 说明 | 需求 | +|------|------|------| +| `webhook` | HTTP Webhook 回调(默认) | 公网 IP 或内网穿透 | +| `websocket` | WebSocket 长连接 | 仅需出网访问能力 | + +## 安装步骤 + +```bash +# 1. 编译已完成,位于 target/release/openfang-cli + +# 2. 安装到系统 +sudo cp target/release/openfang-cli /usr/local/bin/openfang +sudo chmod +x /usr/local/bin/openfang + +# 3. 停止旧版本(如果运行中) +openfang stop + +# 4. 启动新版本 +openfang start + +# 5. 查看日志确认 WebSocket 连接状态 +openfang logs +``` + +## 验证方法 + +### 方法1:检查日志 + +```bash +openfang logs +``` + +期望看到: +``` +Feishu adapter authenticated as [bot名称] +Feishu: Using WebSocket long connection mode +Feishu: Connecting to WebSocket at wss://open.feishu.cn/ws-3 +Feishu: WebSocket hello message sent +``` + +### 方法2:飞书开发者后台配置 + +1. 登录飞书开发者后台 +2. 进入你的应用 +3. 选择 **事件订阅** → **订阅方式** +4. 选择 **使用长连接接收事件** +5. 保存配置 + +配置保存后,飞书会检测长连接是否建立成功。 + +## 实现细节 + +### WebSocket 协议 + +1. **连接端点**: `wss://open.feishu.cn/ws-3` +2. **认证方式**: 发送包含 app_id 和 app_secret 的 JSON 消息 +3. **事件格式**: 与 Webhook 模式相同的 JSON 格式 + +### 自动重连 + +- 连接断开后会自动重连(间隔 5 秒) +- 收到 shutdown 信号时会停止重连 +- 错误会被记录到日志中 + +### 代码变更 + +**新增的文件结构**: +- `openfang-types/src/config.rs`: 添加了 `FeishuConnectionMode` 枚举 +- `openfang-channels/src/feishu.rs`: 添加了 WebSocket 连接逻辑 +- `openfang-api/src/channel_bridge.rs`: 更新了适配器初始化 + +**核心函数**: +```rust +// WebSocket 连接处理 +async fn run_websocket(&self, tx: mpsc::Sender) + -> Result<(), Box> + +// start() 方法根据 connection_mode 分发 +match self.connection_mode { + FeishuConnectionMode::WebSocket => { /* WebSocket 逻辑 */ } + FeishuConnectionMode::Webhook => { /* Webhook 逻辑 */ } +} +``` + +## 与 OpenClaw 的对比 + +| 特性 | OpenClaw (Node.js) | OpenFang (Rust) | +|------|-------------------|----------------| +| SDK | 官方 @larksuiteoapi/node-sdk | 手动实现 | +| WebSocket 支持 | ✅ 原生支持 | ✅ 已实现 | +| 认证方式 | SDK 内部处理 | 手动发送认证消息 | +| 事件解析 | SDK 内部处理 | 手动解析 JSON | + +## 故障排查 + +### 问题:连接失败 + +```bash +# 检查网络 +ping open.feishu.cn + +# 检查应用凭证 +openfang vault list | grep FEISHU +``` + +### 问题:认证失败 + +确认 app_id 和 app_secret 正确: +```bash +# 查看 vault 中的密钥 +openfang vault list +``` + +### 问题:未收到事件 + +1. 确认飞书后台配置了事件订阅 +2. 确认选择了"使用长连接接收事件" +3. 检查日志中是否有 WebSocket 连接成功的日志 + +## 下一步 + +1. 安装新编译的二进制文件 +2. 重启 OpenFang +3. 在飞书后台配置长连接模式 +4. 测试发送消息到飞书应用 + +## 参考文档 + +- [飞书长连接文档](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case) +- [OpenClaw Feishu 实现](~/.nvm/versions/node/v24.13.1/lib/node_modules/openclaw/extensions/feishu/) diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index 9285d0608..ac80e26c5 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -1316,7 +1316,7 @@ pub async fn start_channel_bridge_with_config( let adapter = Arc::new(FeishuAdapter::new( fs_config.app_id.clone(), secret, - fs_config.webhook_port, + fs_config.connection_mode, )); adapters.push((adapter, fs_config.default_agent.clone())); } diff --git a/crates/openfang-channels/Cargo.toml b/crates/openfang-channels/Cargo.toml index 58f0a5df5..aa96452ec 100644 --- a/crates/openfang-channels/Cargo.toml +++ b/crates/openfang-channels/Cargo.toml @@ -26,6 +26,10 @@ hmac = { workspace = true } sha2 = { workspace = true } base64 = { workspace = true } hex = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +flate2 = { workspace = true } +bytes = { workspace = true } lettre = { workspace = true } imap = { workspace = true } diff --git a/crates/openfang-channels/src/feishu.rs b/crates/openfang-channels/src/feishu.rs index 7f4290477..b029caf8c 100644 --- a/crates/openfang-channels/src/feishu.rs +++ b/crates/openfang-channels/src/feishu.rs @@ -1,8 +1,11 @@ //! Feishu/Lark Open Platform channel adapter. //! -//! Uses the Feishu Open API for sending messages and a webhook HTTP server for -//! receiving inbound events. Authentication is performed via a tenant access token -//! obtained from `https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal`. +//! Supports two connection modes: +//! - **Webhook mode**: HTTP server for receiving events (requires public URL) +//! - **WebSocket mode**: Long connection to Feishu server (works from local environment) +//! +//! Authentication is performed via a tenant access token obtained from +//! `https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal`. //! The token is cached and refreshed automatically (2-hour expiry). use crate::types::{ @@ -10,13 +13,16 @@ use crate::types::{ }; use async_trait::async_trait; use chrono::Utc; +use flate2::read::GzDecoder; use futures::Stream; +use openfang_types::config::FeishuConnectionMode; use std::collections::HashMap; +use std::io::Read; use std::pin::Pin; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{mpsc, watch, RwLock}; -use tracing::{info, warn}; +use tracing::{debug, error, info, trace, warn}; use zeroize::Zeroizing; /// Feishu tenant access token endpoint. @@ -38,13 +44,16 @@ const TOKEN_REFRESH_BUFFER_SECS: u64 = 300; /// Feishu/Lark Open Platform adapter. /// /// Inbound messages arrive via a webhook HTTP server that receives event -/// callbacks from the Feishu platform. Outbound messages are sent via the -/// Feishu IM API with a tenant access token for authentication. +/// callbacks from the Feishu platform, or via WebSocket long connection. +/// Outbound messages are sent via the Feishu IM API with a tenant access +/// token for authentication. pub struct FeishuAdapter { /// Feishu app ID. app_id: String, /// SECURITY: Feishu app secret, zeroized on drop. app_secret: Zeroizing, + /// Connection mode (webhook or WebSocket). + connection_mode: FeishuConnectionMode, /// Port on which the inbound webhook HTTP server listens. webhook_port: u16, /// Optional verification token for webhook event validation. @@ -66,13 +75,14 @@ impl FeishuAdapter { /// # Arguments /// * `app_id` - Feishu application ID. /// * `app_secret` - Feishu application secret. - /// * `webhook_port` - Local port for the inbound webhook HTTP server. - pub fn new(app_id: String, app_secret: String, webhook_port: u16) -> Self { + /// * `connection_mode` - Connection mode (webhook or WebSocket). + pub fn new(app_id: String, app_secret: String, connection_mode: FeishuConnectionMode) -> Self { let (shutdown_tx, shutdown_rx) = watch::channel(false); Self { app_id, app_secret: Zeroizing::new(app_secret), - webhook_port, + connection_mode, + webhook_port: 8453, verification_token: None, encrypt_key: None, client: reqwest::Client::new(), @@ -86,11 +96,13 @@ impl FeishuAdapter { pub fn with_verification( app_id: String, app_secret: String, + connection_mode: FeishuConnectionMode, webhook_port: u16, verification_token: Option, encrypt_key: Option, ) -> Self { - let mut adapter = Self::new(app_id, app_secret, webhook_port); + let mut adapter = Self::new(app_id, app_secret, connection_mode); + adapter.webhook_port = webhook_port; adapter.verification_token = verification_token; adapter.encrypt_key = encrypt_key; adapter @@ -179,6 +191,22 @@ impl FeishuAdapter { Ok(bot_name) } + /// Run WebSocket long connection to Feishu server. + async fn run_websocket( + &self, + tx: mpsc::Sender, + ) -> Result<(), Box> { + info!("Feishu: Initializing WebSocket long connection"); + + // Use the correct WebSocket protocol implementation + start_feishu_websocket( + self.app_id.clone(), + self.app_secret.as_str().to_string(), + tx, + ) + .await + } + /// Send a text message to a Feishu chat. async fn api_send_message( &self, @@ -388,144 +416,200 @@ impl ChannelAdapter for FeishuAdapter { info!("Feishu adapter authenticated as {bot_name}"); let (tx, rx) = mpsc::channel::(256); - let port = self.webhook_port; - let verification_token = self.verification_token.clone(); let mut shutdown_rx = self.shutdown_rx.clone(); - tokio::spawn(async move { - let verification_token = Arc::new(verification_token); - let tx = Arc::new(tx); - - let app = axum::Router::new().route( - "/feishu/webhook", - axum::routing::post({ - let vt = Arc::clone(&verification_token); - let tx = Arc::clone(&tx); - move |body: axum::extract::Json| { - let vt = Arc::clone(&vt); - let tx = Arc::clone(&tx); - async move { - // Handle URL verification challenge - if let Some(challenge) = body.0.get("challenge") { - // Verify token if configured - if let Some(ref expected_token) = *vt { - let token = body.0["token"].as_str().unwrap_or(""); - if token != expected_token { - warn!("Feishu: invalid verification token"); - return ( - axum::http::StatusCode::FORBIDDEN, - axum::Json(serde_json::json!({})), - ); + // Route based on connection mode + match self.connection_mode { + FeishuConnectionMode::WebSocket => { + // WebSocket long connection mode + info!("Feishu: Using WebSocket long connection mode"); + let adapter_clone = FeishuAdapter { + app_id: self.app_id.clone(), + app_secret: Zeroizing::new(self.app_secret.as_str().to_string()), + connection_mode: self.connection_mode, + webhook_port: self.webhook_port, + verification_token: self.verification_token.clone(), + encrypt_key: self.encrypt_key.clone(), + client: self.client.clone(), + shutdown_tx: self.shutdown_tx.clone(), + shutdown_rx: self.shutdown_rx.clone(), + cached_token: self.cached_token.clone(), + }; + + tokio::spawn(async move { + info!("Feishu: Starting WebSocket connection task"); + + // Auto-reconnect loop + loop { + tokio::select! { + result = adapter_clone.run_websocket(tx.clone()) => { + match result { + Ok(_) => { + info!("Feishu: WebSocket connection ended normally"); } + Err(e) => { + error!("Feishu: WebSocket error: {e}"); + } + } + // Check if we should stop reconnecting + if *shutdown_rx.borrow() { + info!("Feishu: Shutdown requested, not reconnecting"); + break; } - return ( - axum::http::StatusCode::OK, - axum::Json(serde_json::json!({ - "challenge": challenge, - })), - ); + // Wait before reconnecting + info!("Feishu: Reconnecting in 5 seconds..."); + tokio::time::sleep(Duration::from_secs(5)).await; } - - // Handle event callback - if let Some(schema) = body.0["schema"].as_str() { - if schema == "2.0" { - // V2 event format - if let Some(msg) = parse_feishu_event(&body.0) { - let _ = tx.send(msg).await; + _ = shutdown_rx.changed() => { + info!("Feishu: WebSocket shutdown requested"); + break; + } + } + } + }); + } + FeishuConnectionMode::Webhook => { + // HTTP webhook mode (default) + info!("Feishu: Using HTTP webhook mode"); + let port = self.webhook_port; + let verification_token = self.verification_token.clone(); + + tokio::spawn(async move { + let verification_token = Arc::new(verification_token); + let tx = Arc::new(tx); + + let app = axum::Router::new().route( + "/feishu/webhook", + axum::routing::post({ + let vt = Arc::clone(&verification_token); + let tx = Arc::clone(&tx); + move |body: axum::extract::Json| { + let vt = Arc::clone(&vt); + let tx = Arc::clone(&tx); + async move { + // Handle URL verification challenge + if let Some(challenge) = body.0.get("challenge") { + // Verify token if configured + if let Some(ref expected_token) = *vt { + let token = body.0["token"].as_str().unwrap_or(""); + if token != expected_token { + warn!("Feishu: invalid verification token"); + return ( + axum::http::StatusCode::FORBIDDEN, + axum::Json(serde_json::json!({})), + ); + } + } + return ( + axum::http::StatusCode::OK, + axum::Json(serde_json::json!({ + "challenge": challenge, + })), + ); } - } - } else { - // V1 event format (legacy) - let event_type = body.0["event"]["type"].as_str().unwrap_or(""); - if event_type == "message" { - // Legacy format handling - let event = &body.0["event"]; - let text = event["text"].as_str().unwrap_or(""); - if !text.is_empty() { - let open_id = - event["open_id"].as_str().unwrap_or("").to_string(); - let chat_id = event["open_chat_id"] - .as_str() - .unwrap_or("") - .to_string(); - let msg_id = event["open_message_id"] - .as_str() - .unwrap_or("") - .to_string(); - let is_group = - event["chat_type"].as_str().unwrap_or("") == "group"; - - let content = if text.starts_with('/') { - let parts: Vec<&str> = text.splitn(2, ' ').collect(); - let cmd = parts[0].trim_start_matches('/'); - let args: Vec = parts - .get(1) - .map(|a| { - a.split_whitespace().map(String::from).collect() - }) - .unwrap_or_default(); - ChannelContent::Command { - name: cmd.to_string(), - args, + + // Handle event callback + if let Some(schema) = body.0["schema"].as_str() { + if schema == "2.0" { + // V2 event format + if let Some(msg) = parse_feishu_event(&body.0) { + let _ = tx.send(msg).await; } - } else { - ChannelContent::Text(text.to_string()) - }; - - let channel_msg = ChannelMessage { - channel: ChannelType::Custom("feishu".to_string()), - platform_message_id: msg_id, - sender: ChannelUser { - platform_id: chat_id, - display_name: open_id, - openfang_user: None, - }, - content, - target_agent: None, - timestamp: Utc::now(), - is_group, - thread_id: None, - metadata: HashMap::new(), - }; - - let _ = tx.send(channel_msg).await; + } + } else { + // V1 event format (legacy) + let event_type = body.0["event"]["type"].as_str().unwrap_or(""); + if event_type == "message" { + // Legacy format handling + let event = &body.0["event"]; + let text = event["text"].as_str().unwrap_or(""); + if !text.is_empty() { + let open_id = + event["open_id"].as_str().unwrap_or("").to_string(); + let chat_id = event["open_chat_id"] + .as_str() + .unwrap_or("") + .to_string(); + let msg_id = event["open_message_id"] + .as_str() + .unwrap_or("") + .to_string(); + let is_group = + event["chat_type"].as_str().unwrap_or("") == "group"; + + let content = if text.starts_with('/') { + let parts: Vec<&str> = text.splitn(2, ' ').collect(); + let cmd = parts[0].trim_start_matches('/'); + let args: Vec = parts + .get(1) + .map(|a| { + a.split_whitespace().map(String::from).collect() + }) + .unwrap_or_default(); + ChannelContent::Command { + name: cmd.to_string(), + args, + } + } else { + ChannelContent::Text(text.to_string()) + }; + + let channel_msg = ChannelMessage { + channel: ChannelType::Custom("feishu".to_string()), + platform_message_id: msg_id, + sender: ChannelUser { + platform_id: chat_id, + display_name: open_id, + openfang_user: None, + }, + content, + target_agent: None, + timestamp: Utc::now(), + is_group, + thread_id: None, + metadata: HashMap::new(), + }; + + let _ = tx.send(channel_msg).await; + } + } } + + ( + axum::http::StatusCode::OK, + axum::Json(serde_json::json!({})), + ) } } + }), + ); - ( - axum::http::StatusCode::OK, - axum::Json(serde_json::json!({})), - ) - } - } - }), - ); - - let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port)); - info!("Feishu webhook server listening on {addr}"); + let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port)); + info!("Feishu webhook server listening on {addr}"); - let listener = match tokio::net::TcpListener::bind(addr).await { - Ok(l) => l, - Err(e) => { - warn!("Feishu webhook bind failed: {e}"); - return; - } - }; + let listener = match tokio::net::TcpListener::bind(addr).await { + Ok(l) => l, + Err(e) => { + warn!("Feishu webhook bind failed: {e}"); + return; + } + }; - let server = axum::serve(listener, app); + let server = axum::serve(listener, app); - tokio::select! { - result = server => { - if let Err(e) = result { - warn!("Feishu webhook server error: {e}"); + tokio::select! { + result = server => { + if let Err(e) = result { + warn!("Feishu webhook server error: {e}"); + } + } + _ = shutdown_rx.changed() => { + info!("Feishu adapter shutting down"); + } } - } - _ = shutdown_rx.changed() => { - info!("Feishu adapter shutting down"); - } + }); } - }); + } Ok(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx))) } @@ -560,6 +644,641 @@ impl ChannelAdapter for FeishuAdapter { } } +// ============================================================================ +// Feishu WebSocket Long Connection Implementation +// ============================================================================ + +use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage}; +use futures::{stream::StreamExt, sink::SinkExt}; + +// Protobuf definitions for Feishu WebSocket protocol +pub mod pbbp2 { + use bytes::Buf; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Header { + pub key: String, + pub value: String, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Frame { + pub SeqID: u32, + pub LogID: u32, + pub service: u32, + pub method: u32, + pub headers: Vec
, + pub payload_encoding: String, + pub payload_type: String, + pub payload: Vec, + pub LogIDNew: String, + } + + impl Default for Frame { + fn default() -> Self { + Self { + SeqID: 0, + LogID: 0, + service: 0, + method: 0, + headers: Vec::new(), + payload_encoding: String::new(), + payload_type: String::new(), + payload: Vec::new(), + LogIDNew: String::new(), + } + } + } + + // Simple protobuf encoding helpers + fn encode_varint(mut value: u64, buf: &mut Vec) { + while value >= 0x80 { + buf.push((value as u8) | 0x80); + value >>= 7; + } + buf.push(value as u8); + } + + fn encode_tag(field_number: u32, wire_type: u32, buf: &mut Vec) { + encode_varint((field_number as u64) << 3 | (wire_type as u64), buf); + } + + fn encode_string(field_number: u32, value: &str, buf: &mut Vec) { + let bytes = value.as_bytes(); + encode_tag(field_number, 2, buf); // Length-delimited + encode_varint(bytes.len() as u64, buf); + buf.extend_from_slice(bytes); + } + + fn encode_bytes(field_number: u32, value: &[u8], buf: &mut Vec) { + encode_tag(field_number, 2, buf); // Length-delimited + encode_varint(value.len() as u64, buf); + buf.extend_from_slice(value); + } + + fn encode_uint32(field_number: u32, value: u32, buf: &mut Vec) { + if value == 0 { + return; + } + encode_tag(field_number, 0, buf); // Varint + encode_varint(value as u64, buf); + } + + fn encode_header(field_number: u32, header: &Header, buf: &mut Vec) { + let mut nested = Vec::new(); + encode_string(1, &header.key, &mut nested); + encode_string(2, &header.value, &mut nested); + encode_bytes(field_number, &nested, buf); + } + + impl Frame { + pub fn encode(&self) -> Vec { + let mut buf = Vec::new(); + + encode_uint32(1, self.SeqID, &mut buf); + encode_uint32(2, self.LogID, &mut buf); + encode_uint32(3, self.service, &mut buf); + encode_uint32(4, self.method, &mut buf); + + for header in &self.headers { + encode_header(5, header, &mut buf); + } + + if !self.payload_encoding.is_empty() { + encode_string(6, &self.payload_encoding, &mut buf); + } + if !self.payload_type.is_empty() { + encode_string(7, &self.payload_type, &mut buf); + } + if !self.payload.is_empty() { + encode_bytes(8, &self.payload, &mut buf); + } + if !self.LogIDNew.is_empty() { + encode_string(9, &self.LogIDNew, &mut buf); + } + + buf + } + + pub fn decode(data: &[u8]) -> Result { + let mut frame = Frame::default(); + let mut cursor = std::io::Cursor::new(data); + + while cursor.has_remaining() { + let tag = read_varint(&mut cursor)?; + let field_number = (tag >> 3) as u32; + let wire_type = (tag & 0x07) as u32; + + match field_number { + 1 => frame.SeqID = read_varint(&mut cursor)? as u32, + 2 => frame.LogID = read_varint(&mut cursor)? as u32, + 3 => frame.service = read_varint(&mut cursor)? as u32, + 4 => frame.method = read_varint(&mut cursor)? as u32, + 5 => { + let len = read_varint(&mut cursor)? as usize; + let start = cursor.position() as usize; + let mut header = Header { key: String::new(), value: String::new() }; + + while (cursor.position() as usize - start) < len { + let tag = read_varint(&mut cursor)?; + let field_number = (tag >> 3) as u32; + + if field_number == 1 { + let str_len = read_varint(&mut cursor)? as usize; + header.key = String::from_utf8_lossy(&data[cursor.position() as usize..cursor.position() as usize + str_len]).to_string(); + cursor.advance(str_len); + } else if field_number == 2 { + let str_len = read_varint(&mut cursor)? as usize; + header.value = String::from_utf8_lossy(&data[cursor.position() as usize..cursor.position() as usize + str_len]).to_string(); + cursor.advance(str_len); + } + } + frame.headers.push(header); + } + 6 => { + let len = read_varint(&mut cursor)? as usize; + let pos = cursor.position() as usize; + frame.payload_encoding = String::from_utf8_lossy(&data[pos..pos + len]).to_string(); + cursor.advance(len); + } + 7 => { + let len = read_varint(&mut cursor)? as usize; + let pos = cursor.position() as usize; + frame.payload_type = String::from_utf8_lossy(&data[pos..pos + len]).to_string(); + cursor.advance(len); + } + 8 => { + let len = read_varint(&mut cursor)? as usize; + let pos = cursor.position() as usize; + frame.payload = data[pos..pos + len].to_vec(); + cursor.advance(len); + } + 9 => { + let len = read_varint(&mut cursor)? as usize; + let pos = cursor.position() as usize; + frame.LogIDNew = String::from_utf8_lossy(&data[pos..pos + len]).to_string(); + cursor.advance(len); + } + _ => { + // Skip unknown field + if wire_type == 2 { + let len = read_varint(&mut cursor)? as usize; + cursor.advance(len); + } + } + } + } + + Ok(frame) + } + } + + fn read_varint(cursor: &mut B) -> Result { + let mut result = 0; + let mut shift = 0; + + loop { + if !cursor.has_remaining() { + return Err("Unexpected end of buffer".to_string()); + } + + let byte = cursor.get_u8(); + result |= ((byte & 0x7F) as u64) << shift; + + if byte & 0x80 == 0 { + break; + } + + shift += 7; + if shift >= 64 { + return Err("Varint too large".to_string()); + } + } + + Ok(result) + } +} + +const WS_CONFIG_ENDPOINT: &str = "https://open.feishu.cn/callback/ws/endpoint"; +const FRAME_TYPE_CONTROL: u32 = 0; +const FRAME_TYPE_DATA: u32 = 1; + +#[derive(Debug, Clone, serde::Deserialize)] +struct WsConfigResponse { + code: i32, + data: WsConfigData, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct WsConfigData { + URL: String, + #[serde(rename = "ClientConfig")] + client_config: ClientConfig, +} + +#[derive(Debug, Clone, serde::Deserialize)] +struct ClientConfig { + #[serde(rename = "PingInterval")] + ping_interval: u64, + #[serde(rename = "ReconnectCount")] + reconnect_count: i32, + #[serde(rename = "ReconnectInterval")] + reconnect_interval: u64, + #[serde(rename = "ReconnectNonce")] + reconnect_nonce: u32, +} + +/// Cache for fragmented message data +#[derive(Debug, Default)] +struct DataCache { + fragments: HashMap>>, + expected_seqs: HashMap, +} + +impl DataCache { + fn add_fragment(&mut self, seq: u32, data: Vec) -> Option> { + self.fragments.entry(seq).or_default().push(data); + None + } + + fn complete(&mut self, seq: u32) -> Option> { + if let Some(fragments) = self.fragments.remove(&seq) { + let total_len = fragments.iter().map(|f| f.len()).sum(); + let mut result = Vec::with_capacity(total_len); + for fragment in fragments { + result.extend_from_slice(&fragment); + } + Some(result) + } else { + None + } + } +} + +/// Extract service_id from WebSocket URL +fn extract_service_id(ws_url: &str) -> Option { + url::Url::parse(ws_url) + .ok()? + .query_pairs() + .find(|(k, _)| k == "service_id") + .map(|(_, v)| v.to_string()) +} + +/// Create a protobuf frame for WebSocket communication +fn create_frame( + seq_id: u32, + service: u32, + method: u32, + headers: Vec, + payload: Vec, +) -> Vec { + let frame = pbbp2::Frame { + SeqID: seq_id, + LogID: 0, + service, + method, + headers, + payload_encoding: "gzip".to_string(), + payload_type: "json".to_string(), + payload, + LogIDNew: String::new(), + }; + + frame.encode() +} + +/// Send ping frame to keep connection alive +fn send_ping(seq_id: u32, service_id: &str) -> Vec { + let headers = vec![ + pbbp2::Header { + key: "type".to_string(), + value: "ping".to_string(), + }, + pbbp2::Header { + key: "service_id".to_string(), + value: service_id.to_string(), + }, + ]; + + create_frame(seq_id, 1, FRAME_TYPE_CONTROL, headers, vec![]) +} + +/// Start Feishu WebSocket long connection +pub async fn start_feishu_websocket( + app_id: String, + app_secret: String, + mut tx: mpsc::Sender, +) -> Result<(), Box> { + info!("Feishu: Fetching WebSocket configuration"); + + // Step 1: Fetch WebSocket configuration + let client = reqwest::Client::new(); + let config_response = client + .post(WS_CONFIG_ENDPOINT) + .json(&serde_json::json!({ + "AppID": app_id, + "AppSecret": app_secret + })) + .send() + .await?; + + if !config_response.status().is_success() { + return Err(format!("Failed to fetch WebSocket config: {}", config_response.status()).into()); + } + + let config_data: WsConfigResponse = config_response.json().await?; + if config_data.code != 0 { + return Err(format!("WebSocket config returned error code: {}", config_data.code).into()); + } + + let ws_url = config_data.data.URL; + let ping_interval = config_data.data.client_config.ping_interval; + info!("Feishu: Connecting to WebSocket at {} (ping_interval: {}s)", ws_url, ping_interval); + + // Extract service_id from URL + let service_id = extract_service_id(&ws_url) + .ok_or("Failed to extract service_id from WebSocket URL")?; + + // Step 2: Connect to WebSocket + let (ws_stream, _) = connect_async(&ws_url).await?; + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + + // Step 3: Send initial handshake + let seq_id = 1u32; + let headers = vec![ + pbbp2::Header { + key: "type".to_string(), + value: "register".to_string(), + }, + pbbp2::Header { + key: "service_id".to_string(), + value: service_id.clone(), + }, + ]; + + let register_frame = create_frame( + seq_id, + 1, + FRAME_TYPE_CONTROL, + headers, + vec![], + ); + + ws_sender.send(WsMessage::Binary(register_frame)).await?; + info!("Feishu: WebSocket registration sent"); + + // Step 4: Create channel for ping sending + let (ping_tx, mut ping_rx) = mpsc::channel::>(10); + + // Spawn ping task + let service_id_clone = service_id.clone(); + tokio::spawn(async move { + info!("Feishu: Ping task started, interval={}s", ping_interval); + let mut seq = 2u32; + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(ping_interval)).await; + info!("Feishu: Sending ping frame (seq={})", seq); + let ping = send_ping(seq, &service_id_clone); + if ping_tx.send(ping).await.is_err() { + error!("Feishu: Failed to send ping to channel"); + break; + } + seq = seq.wrapping_add(1); + } + }); + + // Step 5: Message loop with ping handling + let mut data_cache = DataCache::default(); + + info!("Feishu: Entering WebSocket message loop"); + let mut loop_count = 0u64; + + loop { + loop_count += 1; + if loop_count % 100 == 0 { + debug!("Feishu: Message loop iteration {}", loop_count); + } + + tokio::select! { + // Handle WebSocket messages + msg_result = ws_receiver.next() => { + debug!("Feishu: ws_receiver.next() returned"); + match msg_result { + Some(Ok(WsMessage::Binary(data))) => { + info!("Feishu: Received binary WebSocket message, {} bytes", data.len()); + // Decode protobuf frame + match pbbp2::Frame::decode(&*data) { + Ok(frame) => { + info!("Feishu: Received frame: service={}, method={}", frame.service, frame.method); + + // Handle different frame types + if frame.method == FRAME_TYPE_CONTROL { + // Control message (ping/pong/register response) + let frame_type = frame.headers.iter() + .find(|h| h.key == "type") + .map(|h| h.value.as_str()); + + match frame_type { + Some("pong") => { + debug!("Feishu: Received pong"); + } + Some("register") => { + info!("Feishu: Registration confirmed"); + } + _ => { + debug!("Feishu: Unknown control frame type: {:?}", frame_type); + } + } + } else if frame.method == FRAME_TYPE_DATA { + // Data message (actual event) + if !frame.payload.is_empty() { + // Check for fragmented messages + let sum = frame.headers.iter() + .find(|h| h.key == "sum") + .and_then(|h| h.value.parse::().ok()); + + let current_seq = frame.SeqID; + + if let Some(total) = sum { + if total > 1 { + // Fragmented message + data_cache.add_fragment(current_seq, frame.payload.to_vec()); + + if let Some(complete) = data_cache.complete(current_seq) { + // All fragments received, process the complete message + if let Err(e) = process_event_payload(&complete, &mut tx).await { + error!("Feishu: Failed to process event: {}", e); + } + } + } else { + // Single fragment + if let Err(e) = process_event_payload(&frame.payload, &mut tx).await { + error!("Feishu: Failed to process event: {}", e); + } + } + } else { + // No sum header, treat as complete message + if let Err(e) = process_event_payload(&frame.payload, &mut tx).await { + error!("Feishu: Failed to process event: {}", e); + } + } + } + } + } + Err(e) => { + error!("Feishu: Failed to decode protobuf frame: {}", e); + } + } + } + Some(Ok(WsMessage::Close(_))) => { + info!("Feishu: WebSocket closed by server"); + break; + } + Some(Ok(WsMessage::Ping(data))) => { + info!("Feishu: Received WebSocket ping, {} bytes", data.len()); + ws_sender.send(WsMessage::Pong(data)).await?; + } + Some(Ok(WsMessage::Pong(_))) => { + info!("Feishu: Received WebSocket pong"); + } + Some(Ok(WsMessage::Text(text))) => { + info!("Feishu: Received unexpected text message: {}", text); + } + Some(Err(e)) => { + error!("Feishu: WebSocket error: {}", e); + break; + } + None => { + warn!("Feishu: WebSocket channel closed (None received)"); + break; + } + _ => { + warn!("Feishu: Unhandled WebSocket message type"); + } + } + } + // Handle ping messages from the ping task + Some(ping_data) = ping_rx.recv() => { + debug!("Feishu: Sending ping frame to WebSocket"); + if let Err(e) = ws_sender.send(WsMessage::Binary(ping_data)).await { + error!("Feishu: Failed to send ping: {}", e); + break; + } + } + // Both channels closed, exit loop + else => { + warn!("Feishu: Both WebSocket and ping channels closed"); + break; + } + } + } + + info!("Feishu: WebSocket connection ended"); + Ok(()) +} + +/// Process an event payload from Feishu +async fn process_event_payload( + payload: &[u8], + tx: &mut mpsc::Sender, +) -> Result<(), Box> { + // Try to decompress if gzipped + let json_str = if is_gzipped(payload) { + let mut decoder = GzDecoder::new(payload); + let mut decompressed = String::new(); + decoder.read_to_string(&mut decompressed)?; + decompressed + } else { + String::from_utf8(payload.to_vec())? + }; + + info!("Feishu: Event payload: {}", json_str); + + // Parse as JSON to extract event details + let event: serde_json::Value = serde_json::from_str(&json_str)?; + + // Convert to ChannelMessage + if let Err(e) = convert_feishu_event(&event, tx).await { + error!("Feishu: Failed to convert event: {}", e); + } + + Ok(()) +} + +/// Check if bytes are gzipped +fn is_gzipped(data: &[u8]) -> bool { + data.len() > 1 && data[0] == 0x1f && data[1] == 0x8b +} + +/// Convert Feishu event to ChannelMessage +async fn convert_feishu_event( + event: &serde_json::Value, + tx: &mut mpsc::Sender, +) -> Result<(), Box> { + // This is a simplified conversion - real implementation would handle all event types + let header = event.get("header") + .and_then(|v| v.as_object()) + .ok_or("Missing event header")?; + + let event_type = header.get("event_type") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + + if event_type == "im.message.receive_v1" { + let event_data = event.get("event") + .and_then(|v| v.as_object()) + .ok_or("Missing event data")?; + + let message = event_data.get("message") + .and_then(|v| v.as_object()) + .ok_or("Missing message")?; + + let chat_id = message.get("chat_id") + .and_then(|v| v.as_str()) + .ok_or("Missing chat_id")?; + + let content = message.get("content") + .and_then(|v| v.as_str()) + .ok_or("Missing content")?; + + let sender = event_data.get("sender") + .and_then(|v| v.as_object()) + .ok_or("Missing sender")?; + + let sender_id = sender.get("sender_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let user = ChannelUser { + platform_id: chat_id.to_string(), // Use chat_id so responses go to the right place + display_name: sender.get("nickname") + .and_then(|v| v.as_str()) + .unwrap_or(sender_id) + .to_string(), + openfang_user: None, + }; + + let msg = ChannelMessage { + channel: ChannelType::Custom("feishu".to_string()), + platform_message_id: message.get("message_id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + sender: user, + content: ChannelContent::Text(content.to_string()), + target_agent: None, + timestamp: chrono::Utc::now(), + is_group: false, + thread_id: None, + metadata: HashMap::new(), + }; + + tx.send(msg).await?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -567,13 +1286,20 @@ mod tests { #[test] fn test_feishu_adapter_creation() { let adapter = - FeishuAdapter::new("cli_abc123".to_string(), "app-secret-456".to_string(), 9000); + FeishuAdapter::new("cli_abc123".to_string(), "app-secret-456".to_string(), FeishuConnectionMode::Webhook); assert_eq!(adapter.name(), "feishu"); assert_eq!( adapter.channel_type(), ChannelType::Custom("feishu".to_string()) ); - assert_eq!(adapter.webhook_port, 9000); + assert_eq!(adapter.webhook_port, 8453); // default + } + + #[test] + fn test_feishu_adapter_websocket_mode() { + let adapter = + FeishuAdapter::new("cli_abc123".to_string(), "app-secret-456".to_string(), FeishuConnectionMode::WebSocket); + assert_eq!(adapter.connection_mode, FeishuConnectionMode::WebSocket); } #[test] @@ -581,17 +1307,19 @@ mod tests { let adapter = FeishuAdapter::with_verification( "cli_abc123".to_string(), "secret".to_string(), + FeishuConnectionMode::Webhook, 9000, Some("verify-token".to_string()), Some("encrypt-key".to_string()), ); assert_eq!(adapter.verification_token, Some("verify-token".to_string())); assert_eq!(adapter.encrypt_key, Some("encrypt-key".to_string())); + assert_eq!(adapter.webhook_port, 9000); } #[test] fn test_feishu_app_id_stored() { - let adapter = FeishuAdapter::new("cli_test".to_string(), "secret".to_string(), 8080); + let adapter = FeishuAdapter::new("cli_test".to_string(), "secret".to_string(), FeishuConnectionMode::Webhook); assert_eq!(adapter.app_id, "cli_test"); } diff --git a/crates/openfang-runtime/src/subprocess_sandbox.rs b/crates/openfang-runtime/src/subprocess_sandbox.rs index 3e3bce4f0..7b6f9b203 100644 --- a/crates/openfang-runtime/src/subprocess_sandbox.rs +++ b/crates/openfang-runtime/src/subprocess_sandbox.rs @@ -145,8 +145,17 @@ pub fn validate_command_allowlist(command: &str, policy: &ExecPolicy) -> Result< Err("Shell execution is disabled (exec_policy.mode = deny)".to_string()) } ExecSecurityMode::Full => { + // Safely truncate at character boundary + let safe_command = if command.len() > 100 { + command.char_indices() + .nth(100) + .and_then(|(i, _)| Some(&command[..i])) + .unwrap_or(command) + } else { + command + }; tracing::warn!( - command = &command[..command.len().min(100)], + command = &safe_command[..safe_command.len().min(100)], "Shell exec in full mode — no restrictions" ); Ok(()) diff --git a/crates/openfang-types/src/config.rs b/crates/openfang-types/src/config.rs index 617c09bfc..162a2fafa 100644 --- a/crates/openfang-types/src/config.rs +++ b/crates/openfang-types/src/config.rs @@ -2197,6 +2197,22 @@ impl Default for BlueskyConfig { } } +/// Feishu connection mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FeishuConnectionMode { + /// HTTP webhook mode (requires public URL or intranet penetration). + Webhook, + /// WebSocket long connection mode (works from local environment). + WebSocket, +} + +impl Default for FeishuConnectionMode { + fn default() -> Self { + Self::Webhook + } +} + /// Feishu/Lark Open Platform channel adapter configuration. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] @@ -2205,7 +2221,10 @@ pub struct FeishuConfig { pub app_id: String, /// Env var name holding the app secret. pub app_secret_env: String, - /// Port for the incoming webhook. + /// Connection mode (webhook or WebSocket). + #[serde(default)] + pub connection_mode: FeishuConnectionMode, + /// Port for the incoming webhook (only used in webhook mode). pub webhook_port: u16, /// Default agent name to route messages to. pub default_agent: Option, @@ -2219,6 +2238,7 @@ impl Default for FeishuConfig { Self { app_id: String::new(), app_secret_env: "FEISHU_APP_SECRET".to_string(), + connection_mode: FeishuConnectionMode::Webhook, webhook_port: 8453, default_agent: None, overrides: ChannelOverrides::default(),