Files
codeg/src-tauri/src/chat_channel/event_subscriber.rs
2026-03-31 15:26:29 +08:00

214 lines
7.2 KiB
Rust

use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use sea_orm::DatabaseConnection;
use tokio::task::JoinHandle;
use super::i18n::Lang;
use super::manager::ChatChannelManager;
use super::message_formatter;
use super::types::RichMessage;
use crate::db::service::{app_metadata_service, chat_channel_message_log_service, chat_channel_service};
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>,
manager: ChatChannelManager,
db_conn: DatabaseConnection,
) -> JoinHandle<()> {
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 {
Ok(e) => e,
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
eprintln!("[ChatChannel] event subscriber lagged by {n} messages");
continue;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
eprintln!("[ChatChannel] event broadcaster closed, stopping subscriber");
break;
}
};
config.refresh_if_needed(&db_conn).await;
// Prune stale debounce entries
last_push.retain(|_, t| t.elapsed() < Duration::from_secs(DEBOUNCE_SECS * 2));
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;
}
}
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;
}
}
}
// 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;
}
}
// 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;
}
}
}
})
}
fn parse_event(
channel: &str,
payload: &serde_json::Value,
lang: Lang,
) -> Option<(String, RichMessage)> {
match channel {
"acp://event" => parse_acp_event(payload, lang),
_ => None,
}
}
fn parse_acp_event(payload: &serde_json::Value, lang: Lang) -> Option<(String, RichMessage)> {
let event_type = payload.get("type")?.as_str()?;
match event_type {
"turn_complete" => {
let stop_reason = payload
.get("stop_reason")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
// Only push for end_turn, not for intermediate completions
if stop_reason != "end_turn" {
return None;
}
let agent_type = payload
.get("agent_type")
.and_then(|v| v.as_str())
.unwrap_or("Unknown Agent");
Some((
"turn_complete".to_string(),
message_formatter::format_turn_complete(agent_type, stop_reason, lang),
))
}
"error" => {
let agent_type = payload
.get("agent_type")
.and_then(|v| v.as_str())
.unwrap_or("Unknown Agent");
let message = payload
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
Some((
"error".to_string(),
message_formatter::format_agent_error(agent_type, message, lang),
))
}
_ => None,
}
}