diff --git a/src/channels/wechat.rs b/src/channels/wechat.rs index 7ed697c..fd9cd60 100644 --- a/src/channels/wechat.rs +++ b/src/channels/wechat.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; use std::sync::{ - Arc, + Arc, Mutex, atomic::{AtomicBool, Ordering}, }; use std::time::{Duration, UNIX_EPOCH}; @@ -18,6 +18,44 @@ use crate::bus::message::OutboundEventKind; use crate::channels::base::{Channel, ChannelError}; use crate::config::{LLMProviderConfig, WechatChannelConfig}; +/// Rate limiter: ensures minimum interval between messages to the same chat. +static LAST_SEND: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); + +/// Minimum interval between consecutive messages to the same WeChat user. +const MIN_MSG_INTERVAL_MS: u64 = 1500; + +/// Wait if needed to respect the rate limit, then record the send time. +async fn throttle(chat_id: &str) { + let now = tokio::time::Instant::now(); + let sleep_ms = { + let mut map = LAST_SEND.lock().unwrap(); + let delay = map.get(chat_id).and_then(|last| { + let elapsed = now.duration_since(*last); + if elapsed < Duration::from_millis(MIN_MSG_INTERVAL_MS) { + Some(Duration::from_millis(MIN_MSG_INTERVAL_MS) - elapsed) + } else { + None + } + }); + // Record the expected send time now (before sleep), so concurrent + // callers also see the updated time and don't race. + map.insert(chat_id.to_string(), now); + delay + }; + if let Some(ms) = sleep_ms { + tokio::time::sleep(ms).await; + } +} + +/// Update rate-limit timestamp (without waiting) — used after each chunk. +fn touch_rate_limit(chat_id: &str) { + LAST_SEND + .lock() + .unwrap() + .insert(chat_id.to_string(), tokio::time::Instant::now()); +} + #[derive(Clone)] pub struct WechatChannel { name: String, @@ -313,6 +351,9 @@ impl Channel for WechatChannel { let mut text_sent = false; if !text.is_empty() { + // Rate limit: ensure minimum interval between messages to the same user + throttle(&msg.chat_id).await; + let chunks = split_text(&text, MAX_WECHAT_CHUNK_CHARS); if chunks.len() > 1 { tracing::info!( @@ -335,6 +376,8 @@ impl Channel for WechatChannel { error )) })?; + // Update rate-limit timestamp so the next message or chunk respects the gap + touch_rate_limit(&msg.chat_id); tracing::info!( channel = %self.name, chat_id = %msg.chat_id,