mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
Add SILK voice fallback and WeChat polling diagnostics
This commit is contained in:
Generated
+144
-24
@@ -121,6 +121,15 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
@@ -157,7 +166,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -168,7 +177,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -392,6 +401,17 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi 0.1.19",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
@@ -941,6 +961,29 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.59.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"clap 2.34.0",
|
||||
"env_logger",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
"peeking_take_while",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
"shlex",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.66.1"
|
||||
@@ -1360,6 +1403,21 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"strsim 0.8.0",
|
||||
"textwrap",
|
||||
"unicode-width 0.1.14",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.60"
|
||||
@@ -1379,7 +1437,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1388,7 +1446,7 @@ version = "4.5.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap 4.5.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1697,7 +1755,7 @@ dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"clap 4.5.60",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
@@ -1940,7 +1998,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
@@ -2136,7 +2194,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2310,6 +2368,19 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -2323,7 +2394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2848,6 +2919,15 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
@@ -3008,6 +3088,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
@@ -3150,7 +3236,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.3",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -3407,7 +3493,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"clap",
|
||||
"clap 4.5.60",
|
||||
"clap_complete",
|
||||
"criterion",
|
||||
"cron",
|
||||
@@ -3459,6 +3545,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_yml",
|
||||
"sha2",
|
||||
"silk-rs",
|
||||
"subtle",
|
||||
"tar",
|
||||
"tempfile",
|
||||
@@ -3522,9 +3609,9 @@ version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.5.2",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3758,7 +3845,7 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f2a50a585a1184a43621a9133b7702ba5cb7a87ca5e704056b19d8005de6faf"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"bindgen 0.66.1",
|
||||
"cc",
|
||||
]
|
||||
|
||||
@@ -4144,7 +4231,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4232,7 +4319,7 @@ version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.5.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -4710,7 +4797,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.5.2",
|
||||
"pin-project-lite",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -4930,7 +5017,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.37",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.3",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -4967,9 +5054,9 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.3",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5482,7 +5569,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6108,6 +6195,18 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "silk-rs"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "014e6619f35a385ff848570e73a0b8c36b31031e0ee11cac70192b50097b1cfe"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"bytes",
|
||||
"cc",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
@@ -6164,7 +6263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6236,6 +6335,12 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -6389,7 +6494,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6476,6 +6581,15 @@ dependencies = [
|
||||
"testcontainers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -7189,7 +7303,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7342,6 +7456,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -8039,7 +8159,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -175,6 +175,7 @@ base64 = "0.22.1"
|
||||
mime_guess = "2.0.5"
|
||||
clap_complete = "4.5.0"
|
||||
lru = "0.16.3"
|
||||
silk-rs = "0.2.0"
|
||||
|
||||
# HTML to Markdown conversion (feature gated)
|
||||
html-to-markdown-rs = { version = "2.3", optional = true }
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
|
||||
| Linq | ✅ | ❌ | P3 | Real iMessage via API, no Mac required |
|
||||
| Feishu/Lark | ✅ | 🚧 | P3 | WASM channel with Event Subscription v2.0; Bitable/Docx tools planned |
|
||||
| LINE | ✅ | ❌ | P3 | |
|
||||
| WeChat (iLink bot) | ✅ | 🚧 | P2 | Extension-first channel (`channels-src/wechat`), single-account DM flow with QR login, typing, image send/receive, inbound file extraction, and inbound voice attachment passthrough; multi-account plus fuller voice/video and outbound file parity follow-up |
|
||||
| WeChat (iLink bot) | ✅ | 🚧 | P2 | Extension-first channel (`channels-src/wechat`), single-account DM flow with QR login, typing, image send/receive, inbound file extraction, and inbound voice handling with SILK-to-WAV fallback; multi-account plus video and outbound file parity follow-up |
|
||||
| WebChat | ✅ | ✅ | - | Web gateway chat |
|
||||
| Matrix | ✅ | ❌ | P3 | E2EE support |
|
||||
| Mattermost | ✅ | ❌ | P3 | Emoji reactions, interactive buttons, model picker |
|
||||
|
||||
@@ -138,6 +138,16 @@ impl Guest for WechatChannel {
|
||||
|
||||
let mut context_tokens_changed = false;
|
||||
for message in response.msgs {
|
||||
log_channel(
|
||||
channel_host::LogLevel::Info,
|
||||
&format!(
|
||||
"Received WeChat message: id={:?} from_user_id={:?} message_type={:?} item_types=[{}]",
|
||||
message.message_id,
|
||||
message.from_user_id,
|
||||
message.message_type,
|
||||
summarize_item_types(&message),
|
||||
),
|
||||
);
|
||||
if let Some(from_user_id) = message.from_user_id.as_deref() {
|
||||
if let Some(context_token) = message.context_token.as_deref() {
|
||||
let changed = context_tokens
|
||||
@@ -149,6 +159,15 @@ impl Guest for WechatChannel {
|
||||
}
|
||||
match incoming_bundle_from_message(&config, message) {
|
||||
Ok(Some(bundle)) => {
|
||||
log_channel(
|
||||
channel_host::LogLevel::Info,
|
||||
&format!(
|
||||
"Mapped WeChat message into bundle: from_user_id={} text_len={} attachment_count={}",
|
||||
bundle.from_user_id,
|
||||
bundle.text.trim().chars().count(),
|
||||
bundle.attachments.len(),
|
||||
),
|
||||
);
|
||||
let emitted = process_incoming_bundle(
|
||||
&mut pending_inbound,
|
||||
bundle,
|
||||
@@ -369,6 +388,15 @@ fn process_incoming_bundle(
|
||||
}
|
||||
|
||||
fn emit_buffered_bundle(bundle: PendingInboundBundle) {
|
||||
log_channel(
|
||||
channel_host::LogLevel::Info,
|
||||
&format!(
|
||||
"Emitting WeChat bundle to agent: from_user_id={} text_len={} attachment_count={}",
|
||||
bundle.from_user_id,
|
||||
bundle.text.trim().chars().count(),
|
||||
bundle.attachments.len(),
|
||||
),
|
||||
);
|
||||
let metadata = json!({
|
||||
"from_user_id": bundle.from_user_id,
|
||||
"to_user_id": bundle.to_user_id,
|
||||
@@ -413,6 +441,27 @@ fn merge_text(existing: &str, incoming: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn summarize_item_types(message: &WechatMessage) -> String {
|
||||
let item_types = message
|
||||
.item_list
|
||||
.iter()
|
||||
.map(|item| match item.r#type {
|
||||
Some(MESSAGE_ITEM_TEXT) => "text".to_string(),
|
||||
Some(crate::types::MESSAGE_ITEM_IMAGE) => "image".to_string(),
|
||||
Some(crate::types::MESSAGE_ITEM_VOICE) => "voice".to_string(),
|
||||
Some(crate::types::MESSAGE_ITEM_FILE) => "file".to_string(),
|
||||
Some(other) => format!("unknown:{other}"),
|
||||
None => "missing".to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if item_types.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
item_types.join(",")
|
||||
}
|
||||
}
|
||||
|
||||
fn send_response(
|
||||
config: &WechatConfig,
|
||||
metadata: &OutboundMetadata,
|
||||
|
||||
@@ -31,7 +31,7 @@ fn default_poll_interval_ms() -> u32 {
|
||||
}
|
||||
|
||||
fn default_long_poll_timeout_ms() -> u32 {
|
||||
35_000
|
||||
15_000
|
||||
}
|
||||
|
||||
impl Default for WechatConfig {
|
||||
|
||||
@@ -45,6 +45,6 @@
|
||||
"cdn_base_url": "https://novac2c.cdn.weixin.qq.com/c2c",
|
||||
"bot_type": "3",
|
||||
"poll_interval_ms": 30000,
|
||||
"long_poll_timeout_ms": 35000
|
||||
"long_poll_timeout_ms": 15000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ The point of this phase is to keep the channel aligned with upstream behavior wh
|
||||
|
||||
These are upstream features, so they belong on the roadmap, but they do not need to block the first implementation cut:
|
||||
|
||||
- broader media parity beyond the current image + inbound-file + inbound-voice path (outbound files, fuller voice handling, and video)
|
||||
- broader media parity beyond the current image + inbound-file + inbound-voice path (outbound files and video)
|
||||
|
||||
We should not spend time listing non-goals that come from outside the upstream capability boundary.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use aes::Aes128;
|
||||
use aes::cipher::{BlockDecrypt, KeyInit, generic_array::GenericArray};
|
||||
use base64::Engine as _;
|
||||
use serde::Deserialize;
|
||||
use silk_rs::decode_silk;
|
||||
|
||||
use crate::channels::wasm::capabilities::ChannelCapabilities;
|
||||
use crate::channels::wasm::host::{Attachment, ChannelHostState};
|
||||
@@ -11,6 +12,7 @@ use crate::channels::wasm::host::{Attachment, ChannelHostState};
|
||||
const AES_BLOCK_SIZE: usize = 16;
|
||||
const MAX_ATTACHMENT_BYTES: usize = 20 * 1024 * 1024;
|
||||
const WECHAT_CHANNEL_NAME: &str = "wechat";
|
||||
const WECHAT_SILK_SAMPLE_RATE_HZ: i32 = 24_000;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WechatAttachmentExtras {
|
||||
@@ -43,10 +45,19 @@ pub(crate) async fn hydrate_attachment_for_channel(
|
||||
Ok(ciphertext) => match decrypt_wechat_attachment_bytes(&ciphertext, &encoded_aes_key) {
|
||||
Ok(plaintext) => {
|
||||
attachment.size_bytes = Some(plaintext.len() as u64);
|
||||
if attachment.mime_type.starts_with("image/") {
|
||||
attachment.mime_type = detect_image_mime(&plaintext).to_string();
|
||||
}
|
||||
attachment.data = plaintext;
|
||||
if attachment.mime_type.starts_with("image/") {
|
||||
attachment.mime_type = detect_image_mime(&attachment.data).to_string();
|
||||
} else if is_wechat_silk_attachment(attachment)
|
||||
&& let Err(error) = maybe_transcode_wechat_silk_attachment(attachment)
|
||||
{
|
||||
tracing::warn!(
|
||||
channel = %channel_name,
|
||||
attachment_id = %attachment.id,
|
||||
error = %error,
|
||||
"Failed to transcode WeChat SILK attachment; preserving raw SILK"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
@@ -68,6 +79,15 @@ pub(crate) async fn hydrate_attachment_for_channel(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_wechat_silk_attachment(attachment: &Attachment) -> bool {
|
||||
attachment.mime_type.eq_ignore_ascii_case("audio/silk")
|
||||
|| attachment
|
||||
.filename
|
||||
.as_deref()
|
||||
.and_then(|filename| filename.rsplit_once('.').map(|(_, ext)| ext))
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("silk"))
|
||||
}
|
||||
|
||||
fn should_hydrate_wechat_attachment(channel_name: &str, attachment: &Attachment) -> bool {
|
||||
channel_name == WECHAT_CHANNEL_NAME
|
||||
&& attachment.data.is_empty()
|
||||
@@ -230,6 +250,68 @@ fn detect_image_mime(bytes: &[u8]) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_transcode_wechat_silk_attachment(attachment: &mut Attachment) -> Result<(), String> {
|
||||
if attachment.data.is_empty() {
|
||||
return Err("SILK attachment has no data".to_string());
|
||||
}
|
||||
|
||||
let pcm = decode_silk(&attachment.data, WECHAT_SILK_SAMPLE_RATE_HZ)
|
||||
.map_err(|error| format!("SILK decode failed: {error}"))?;
|
||||
if pcm.is_empty() {
|
||||
return Err("SILK decoder returned empty PCM".to_string());
|
||||
}
|
||||
|
||||
let wav = pcm_s16le_to_wav(&pcm, WECHAT_SILK_SAMPLE_RATE_HZ as u32)?;
|
||||
attachment.data = wav;
|
||||
attachment.size_bytes = Some(attachment.data.len() as u64);
|
||||
attachment.mime_type = "audio/wav".to_string();
|
||||
if let Some(filename) = attachment.filename.as_mut() {
|
||||
replace_attachment_extension(filename, "wav");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pcm_s16le_to_wav(pcm: &[u8], sample_rate_hz: u32) -> Result<Vec<u8>, String> {
|
||||
if !pcm.len().is_multiple_of(2) {
|
||||
return Err("PCM buffer length must be even for 16-bit mono audio".to_string());
|
||||
}
|
||||
|
||||
let data_len = u32::try_from(pcm.len())
|
||||
.map_err(|_| "PCM buffer exceeds WAV container size limits".to_string())?;
|
||||
let total_len = 44u32
|
||||
.checked_add(data_len)
|
||||
.ok_or_else(|| "WAV container size overflowed".to_string())?;
|
||||
let byte_rate = sample_rate_hz
|
||||
.checked_mul(2)
|
||||
.ok_or_else(|| "WAV byte rate overflowed".to_string())?;
|
||||
|
||||
let mut wav = Vec::with_capacity(total_len as usize);
|
||||
wav.extend_from_slice(b"RIFF");
|
||||
wav.extend_from_slice(&(total_len - 8).to_le_bytes());
|
||||
wav.extend_from_slice(b"WAVE");
|
||||
wav.extend_from_slice(b"fmt ");
|
||||
wav.extend_from_slice(&16u32.to_le_bytes());
|
||||
wav.extend_from_slice(&1u16.to_le_bytes());
|
||||
wav.extend_from_slice(&1u16.to_le_bytes());
|
||||
wav.extend_from_slice(&sample_rate_hz.to_le_bytes());
|
||||
wav.extend_from_slice(&byte_rate.to_le_bytes());
|
||||
wav.extend_from_slice(&2u16.to_le_bytes());
|
||||
wav.extend_from_slice(&16u16.to_le_bytes());
|
||||
wav.extend_from_slice(b"data");
|
||||
wav.extend_from_slice(&data_len.to_le_bytes());
|
||||
wav.extend_from_slice(pcm);
|
||||
Ok(wav)
|
||||
}
|
||||
|
||||
fn replace_attachment_extension(filename: &mut String, replacement: &str) {
|
||||
if let Some((stem, _)) = filename.rsplit_once('.') {
|
||||
*filename = format!("{stem}.{replacement}");
|
||||
} else {
|
||||
filename.push('.');
|
||||
filename.push_str(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn encrypt_aes_ecb_pkcs7(plaintext: &[u8], key: &[u8]) -> Result<Vec<u8>, String> {
|
||||
use aes::cipher::BlockEncrypt;
|
||||
@@ -250,7 +332,8 @@ fn encrypt_aes_ecb_pkcs7(plaintext: &[u8], key: &[u8]) -> Result<Vec<u8>, String
|
||||
mod tests {
|
||||
use super::{
|
||||
Attachment, decrypt_wechat_attachment_bytes, detect_image_mime, encrypt_aes_ecb_pkcs7,
|
||||
hydrate_attachment_for_channel, should_hydrate_wechat_attachment,
|
||||
hydrate_attachment_for_channel, maybe_transcode_wechat_silk_attachment, pcm_s16le_to_wav,
|
||||
should_hydrate_wechat_attachment,
|
||||
};
|
||||
use crate::channels::wasm::ChannelCapabilities;
|
||||
use base64::Engine as _;
|
||||
@@ -315,4 +398,39 @@ mod tests {
|
||||
assert!(attachment.data.is_empty());
|
||||
assert_eq!(attachment.size_bytes, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcm_s16le_to_wav_wraps_pcm_with_expected_header() {
|
||||
let wav = pcm_s16le_to_wav(&[0x00, 0x00, 0x01, 0x00], 24_000).expect("wav wrapping");
|
||||
assert!(wav.starts_with(b"RIFF"));
|
||||
assert_eq!(&wav[8..12], b"WAVE");
|
||||
assert_eq!(&wav[12..16], b"fmt ");
|
||||
assert_eq!(&wav[36..40], b"data");
|
||||
assert_eq!(&wav[40..44], &(4u32).to_le_bytes());
|
||||
assert_eq!(&wav[44..], &[0x00, 0x00, 0x01, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn silk_transcode_failure_preserves_raw_silk_path_for_callers() {
|
||||
let mut attachment = Attachment {
|
||||
id: "wechat-voice-1".to_string(),
|
||||
mime_type: "audio/silk".to_string(),
|
||||
filename: Some("wechat-voice-1.silk".to_string()),
|
||||
size_bytes: Some(3),
|
||||
source_url: None,
|
||||
storage_key: None,
|
||||
extracted_text: None,
|
||||
extras_json: encode_test_extras_json("ZmFrZS1rZXk="),
|
||||
data: vec![1, 2, 3],
|
||||
duration_secs: Some(1),
|
||||
};
|
||||
|
||||
let original = attachment.data.clone();
|
||||
let error =
|
||||
maybe_transcode_wechat_silk_attachment(&mut attachment).expect_err("invalid SILK");
|
||||
assert!(error.contains("SILK decode failed"));
|
||||
assert_eq!(attachment.mime_type, "audio/silk");
|
||||
assert_eq!(attachment.filename.as_deref(), Some("wechat-voice-1.silk"));
|
||||
assert_eq!(attachment.data, original);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user