优化消息渠道的实现代码
This commit is contained in:
@@ -14,7 +14,68 @@ use crate::web::event_bridge::WebEventBroadcaster;
|
||||
|
||||
/// Minimum interval between pushes for the same event type per channel (debounce).
|
||||
const DEBOUNCE_SECS: u64 = 5;
|
||||
/// How often to refresh cached config from DB.
|
||||
const CONFIG_CACHE_TTL_SECS: u64 = 30;
|
||||
|
||||
const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language";
|
||||
const EVENT_FILTER_KEY: &str = "chat_event_filter";
|
||||
|
||||
struct CachedChannel {
|
||||
id: i32,
|
||||
event_filter_json: Option<String>,
|
||||
}
|
||||
|
||||
struct EventConfigCache {
|
||||
lang: Lang,
|
||||
global_filter: Option<Vec<String>>,
|
||||
enabled_channels: Vec<CachedChannel>,
|
||||
last_refresh: Instant,
|
||||
}
|
||||
|
||||
impl EventConfigCache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
lang: Lang::default(),
|
||||
global_filter: None,
|
||||
enabled_channels: Vec::new(),
|
||||
// Force refresh on first use
|
||||
last_refresh: Instant::now() - Duration::from_secs(CONFIG_CACHE_TTL_SECS + 1),
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_if_needed(&mut self, db: &DatabaseConnection) {
|
||||
if self.last_refresh.elapsed() < Duration::from_secs(CONFIG_CACHE_TTL_SECS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(Some(val)) = app_metadata_service::get_value(db, MESSAGE_LANGUAGE_KEY).await {
|
||||
self.lang = Lang::from_str_lossy(&val);
|
||||
}
|
||||
|
||||
// Parse as Option<Vec<String>> so JSON "null" → None (intentional, not accidental)
|
||||
self.global_filter = app_metadata_service::get_value(db, EVENT_FILTER_KEY)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|json| {
|
||||
serde_json::from_str::<Option<Vec<String>>>(&json)
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
if let Ok(channels) = chat_channel_service::list_enabled(db).await {
|
||||
self.enabled_channels = channels
|
||||
.into_iter()
|
||||
.map(|ch| CachedChannel {
|
||||
id: ch.id,
|
||||
event_filter_json: ch.event_filter_json,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
self.last_refresh = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_event_subscriber(
|
||||
broadcaster: Arc<WebEventBroadcaster>,
|
||||
@@ -24,6 +85,7 @@ pub fn spawn_event_subscriber(
|
||||
tokio::spawn(async move {
|
||||
let mut rx = broadcaster.subscribe();
|
||||
let mut last_push: HashMap<(i32, String), Instant> = HashMap::new();
|
||||
let mut config = EventConfigCache::new();
|
||||
|
||||
loop {
|
||||
let event = match rx.recv().await {
|
||||
@@ -38,82 +100,72 @@ pub fn spawn_event_subscriber(
|
||||
}
|
||||
};
|
||||
|
||||
let lang = load_lang(&db_conn).await;
|
||||
config.refresh_if_needed(&db_conn).await;
|
||||
|
||||
let message = match parse_event(&event.channel, &event.payload, lang) {
|
||||
Some((event_type, msg)) => {
|
||||
// Global event filter check
|
||||
let global_filter = app_metadata_service::get_value(&db_conn, "chat_event_filter")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|json| serde_json::from_str::<Vec<String>>(&json).ok());
|
||||
// Prune stale debounce entries
|
||||
last_push.retain(|_, t| t.elapsed() < Duration::from_secs(DEBOUNCE_SECS * 2));
|
||||
|
||||
if let Some(filter) = &global_filter {
|
||||
if !filter.contains(&event_type) {
|
||||
continue;
|
||||
}
|
||||
if let Some((event_type, msg)) =
|
||||
parse_event(&event.channel, &event.payload, config.lang)
|
||||
{
|
||||
// Global event filter check
|
||||
if let Some(filter) = &config.global_filter {
|
||||
if !filter.contains(&event_type) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check enabled channels and forward
|
||||
let channels = match chat_channel_service::list_enabled(&db_conn).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[ChatChannel] failed to list channels: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for ch in &channels {
|
||||
// Debounce
|
||||
let key = (ch.id, event_type.clone());
|
||||
let now = Instant::now();
|
||||
if let Some(last) = last_push.get(&key) {
|
||||
if now.duration_since(*last) < Duration::from_secs(DEBOUNCE_SECS) {
|
||||
for ch in &config.enabled_channels {
|
||||
// Per-channel event filter
|
||||
if let Some(filter_json) = &ch.event_filter_json {
|
||||
if let Ok(filter) = serde_json::from_str::<Vec<String>>(filter_json) {
|
||||
if !filter.contains(&event_type) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
last_push.insert(key, now);
|
||||
|
||||
// Send
|
||||
let send_result = manager.send_to_channel(ch.id, &msg).await;
|
||||
let (status, error_detail) = match &send_result {
|
||||
Ok(_) => ("sent", None),
|
||||
Err(e) => ("failed", Some(e.to_string())),
|
||||
};
|
||||
|
||||
let _ = chat_channel_message_log_service::create_log(
|
||||
&db_conn,
|
||||
ch.id,
|
||||
"outbound",
|
||||
"event_push",
|
||||
&msg.to_plain_text(),
|
||||
status,
|
||||
error_detail,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Some(msg)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
// Debounce: skip if same event type was pushed to this channel recently
|
||||
let key = (ch.id, event_type.clone());
|
||||
let now = Instant::now();
|
||||
if let Some(last) = last_push.get(&key) {
|
||||
if now.duration_since(*last) < Duration::from_secs(DEBOUNCE_SECS) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
drop(message);
|
||||
// Send
|
||||
let send_result = manager.send_to_channel(ch.id, &msg).await;
|
||||
let (status, error_detail) = match &send_result {
|
||||
Ok(_) => {
|
||||
// Only update debounce timestamp on success
|
||||
last_push.insert(key, now);
|
||||
("sent", None)
|
||||
}
|
||||
Err(e) => ("failed", Some(e.to_string())),
|
||||
};
|
||||
|
||||
let _ = chat_channel_message_log_service::create_log(
|
||||
&db_conn,
|
||||
ch.id,
|
||||
"outbound",
|
||||
"event_push",
|
||||
&msg.to_plain_text(),
|
||||
status,
|
||||
error_detail,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn load_lang(db: &DatabaseConnection) -> Lang {
|
||||
app_metadata_service::get_value(db, MESSAGE_LANGUAGE_KEY)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| Lang::from_str_lossy(&v))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn parse_event(channel: &str, payload: &serde_json::Value, lang: Lang) -> Option<(String, RichMessage)> {
|
||||
fn parse_event(
|
||||
channel: &str,
|
||||
payload: &serde_json::Value,
|
||||
lang: Lang,
|
||||
) -> Option<(String, RichMessage)> {
|
||||
match channel {
|
||||
"acp://event" => parse_acp_event(payload, lang),
|
||||
_ => None,
|
||||
|
||||
Reference in New Issue
Block a user