use crate::*; /// AI 任务事件类型。 /// /// 事件表用于给订阅端和 BFF 增量消费状态变化;正式任务真相仍以 /// `ai_task`、`ai_task_stage`、`ai_text_chunk` 和 `ai_result_reference` 为准。 #[derive(Clone, Copy, Debug, PartialEq, Eq, SpacetimeType)] pub enum AiTaskEventKind { TaskCreated, TaskStatusChanged, StageStarted, StageCompleted, TextChunkAppended, ResultReferenceAttached, } #[spacetimedb::table( accessor = ai_task_event, public, event, index(accessor = by_ai_task_event_task_id, btree(columns = [task_id])), index(accessor = by_ai_task_event_owner_user_id, btree(columns = [owner_user_id])) )] pub struct AiTaskEvent { #[primary_key] pub(crate) event_id: String, pub(crate) task_id: String, pub(crate) owner_user_id: String, pub(crate) event_kind: AiTaskEventKind, pub(crate) task_status: Option, pub(crate) stage_kind: Option, pub(crate) text_chunk_row_id: Option, pub(crate) result_reference_row_id: Option, pub(crate) occurred_at: Timestamp, } pub(crate) fn emit_ai_task_event( ctx: &ReducerContext, task: &AiTaskSnapshot, event_kind: AiTaskEventKind, stage_kind: Option, text_chunk_row_id: Option, result_reference_row_id: Option, occurred_at_micros: i64, ) { let suffix = match event_kind { AiTaskEventKind::TaskCreated => "created".to_string(), AiTaskEventKind::TaskStatusChanged => format!("status_{}", task.status.as_event_slug()), AiTaskEventKind::StageStarted => { format!("stage_started_{}", stage_kind_slug(stage_kind)) } AiTaskEventKind::StageCompleted => { format!("stage_completed_{}", stage_kind_slug(stage_kind)) } AiTaskEventKind::TextChunkAppended => { format!( "chunk_{}", text_chunk_row_id.as_deref().unwrap_or("unknown") ) } AiTaskEventKind::ResultReferenceAttached => { format!( "result_{}", result_reference_row_id.as_deref().unwrap_or("unknown") ) } }; ctx.db.ai_task_event().insert(AiTaskEvent { event_id: format!("aievt_{}_{}_{}", task.task_id, occurred_at_micros, suffix), task_id: task.task_id.clone(), owner_user_id: task.owner_user_id.clone(), event_kind, task_status: Some(task.status), stage_kind, text_chunk_row_id, result_reference_row_id, occurred_at: Timestamp::from_micros_since_unix_epoch(occurred_at_micros), }); } fn stage_kind_slug(stage_kind: Option) -> &'static str { stage_kind.map(AiTaskStageKind::as_str).unwrap_or("unknown") } trait AiTaskStatusEventSlug { fn as_event_slug(self) -> &'static str; } impl AiTaskStatusEventSlug for AiTaskStatus { fn as_event_slug(self) -> &'static str { match self { Self::Pending => "pending", Self::Running => "running", Self::Completed => "completed", Self::Failed => "failed", Self::Cancelled => "cancelled", } } }