feat: 实现长消息分块发送功能,优化 WeChat 消息传递

This commit is contained in:
ooodc 2026-06-06 10:24:55 +08:00
parent fb90641774
commit c3bfe32fa3

View File

@ -5,7 +5,7 @@ use std::sync::{
Arc, Arc,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
}; };
use std::time::UNIX_EPOCH; use std::time::{Duration, UNIX_EPOCH};
use async_trait::async_trait; use async_trait::async_trait;
use futures_util::FutureExt; use futures_util::FutureExt;
@ -313,15 +313,37 @@ impl Channel for WechatChannel {
let mut text_sent = false; let mut text_sent = false;
if !text.is_empty() { if !text.is_empty() {
self.bot.send(&msg.chat_id, &text).await.map_err(|error| { let chunks = split_text(&text, MAX_WECHAT_CHUNK_CHARS);
ChannelError::SendError(format!("WeChat text send failed: {}", error)) if chunks.len() > 1 {
tracing::info!(
channel = %self.name,
chat_id = %msg.chat_id,
total_chars = text.len(),
chunk_count = chunks.len(),
"WeChat: splitting long message into chunks"
);
}
for (i, chunk) in chunks.iter().enumerate() {
if i > 0 {
tokio::time::sleep(Duration::from_millis(CHUNK_SEND_INTERVAL_MS)).await;
}
self.bot.send(&msg.chat_id, chunk).await.map_err(|error| {
ChannelError::SendError(format!(
"WeChat text send failed (chunk {}/{}): {}",
i + 1,
chunks.len(),
error
))
})?; })?;
tracing::info!( tracing::info!(
channel = %self.name, channel = %self.name,
chat_id = %msg.chat_id, chat_id = %msg.chat_id,
content_len = text.len(), chunk = i + 1,
total_chunks = chunks.len(),
content_len = chunk.len(),
"WeChat text message sent" "WeChat text message sent"
); );
}
text_sent = true; text_sent = true;
} }
@ -356,6 +378,51 @@ impl Channel for WechatChannel {
} }
} }
/// Split text into chunks suitable for WeChat delivery.
/// Prefers splitting at paragraph breaks, then newlines, then sentence boundaries.
const MAX_WECHAT_CHUNK_CHARS: usize = 2000;
const CHUNK_SEND_INTERVAL_MS: u64 = 500;
fn split_text(text: &str, limit: usize) -> Vec<String> {
if text.len() <= limit {
return vec![text.to_string()];
}
let mut chunks = Vec::new();
let mut remaining = text;
while !remaining.is_empty() {
if remaining.len() <= limit {
chunks.push(remaining.to_string());
break;
}
let end = remaining.floor_char_boundary(limit);
let window = &remaining[..end];
let cut = window
.rfind("\n\n")
.filter(|&i| i > end * 3 / 10)
.map(|i| i + 2)
.or_else(|| {
window
.rfind('\n')
.filter(|&i| i > end * 3 / 10)
.map(|i| i + 1)
})
.or_else(|| {
window
.rfind('。')
.filter(|&i| i > end * 3 / 10)
.map(|i| i + 3)
})
.unwrap_or(end);
chunks.push(remaining[..cut].to_string());
remaining = &remaining[cut..];
}
if chunks.is_empty() {
vec![String::new()]
} else {
chunks
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;