//! 知识生命线记录器 — 统一入口,各业务流程只调一行 //! //! 设计目标: //! - 事件记录逻辑不散落在各业务流程内部(提取/注入/状态变更),集中于此模块 //! - fire-and-forget 友好:便捷方法内部吞掉错误(只 warn),调用方无需处理 Result //! - 易扩展:新事件类型只加一个便捷方法 + 一行 context 构造,不改主流程 use std::sync::Arc; use df_types::types::new_id; use df_storage::crud::KnowledgeEventsRepo; use df_storage::db::Database; use df_storage::models::KnowledgeEventRecord; use super::{err_str, now_millis}; /// 事件类型常量(生命线节点;归档复用 status_changed 的 to=archived,不单列) pub const EVENT_CREATED: &str = "created"; pub const EVENT_EXTRACTED: &str = "extracted"; pub const EVENT_STATUS_CHANGED: &str = "status_changed"; pub const EVENT_REFERENCED: &str = "referenced"; /// 知识生命线记录器 — 持有 DB 句柄,提供便捷事件写入方法 /// /// 用法:`KnowledgeTimeline::new(&state.db).record_xxx(...).await` /// /// 异步策略(按路径热度,非 bug,刻意不一致): /// - 热路径(对话注入 build_knowledge_context / 提炼 extract):整块已在 spawn 任务内, /// 此处直接 await = 对用户 fire-and-forget,不阻塞 AI 对话 /// - 低频命令(状态变更 / 创建):事件写入 ms 级,直接 await 比 detached spawn 更简单 /// 便捷方法内部 `fire()` 已吞错误(warn 不阻断),调用方无需处理 Result。 pub struct KnowledgeTimeline { db: Arc, } impl KnowledgeTimeline { pub fn new(db: &Arc) -> Self { Self { db: db.clone() } } /// 通用写入(底层入口,便捷方法均经此) pub async fn record( &self, knowledge_id: &str, event_type: &str, source_ref: Option, context_json: Option, ) -> Result<(), String> { let record = KnowledgeEventRecord { id: new_id(), knowledge_id: knowledge_id.to_string(), event_type: event_type.to_string(), source_ref, context_json, timestamp: now_millis(), }; KnowledgeEventsRepo::new(&self.db) .insert(record) .await .map_err(err_str)?; Ok(()) } /// 手动录入产生(context: method) pub async fn record_created(&self, knowledge_id: &str, method: &str) { let ctx = serde_json::json!({ "method": method }).to_string(); self.fire(knowledge_id, EVENT_CREATED, Some("manual".into()), Some(ctx)).await; } /// AI 对话提炼产生(context: conv_id / conv_title / reasoning) pub async fn record_extracted( &self, knowledge_id: &str, conv_id: &str, conv_title: &str, reasoning: &str, ) { let ctx = serde_json::json!({ "conv_id": conv_id, "conv_title": conv_title, "reasoning": reasoning, }) .to_string(); self.fire( knowledge_id, EVENT_EXTRACTED, Some(format!("conv:{}", conv_id)), Some(ctx), ) .await; } /// 被检索命中/注入(context: conv_id / query) pub async fn record_referenced(&self, knowledge_id: &str, conv_id: &str, query: &str) { let ctx = serde_json::json!({ "conv_id": conv_id, "query": query }).to_string(); self.fire( knowledge_id, EVENT_REFERENCED, Some(format!("conv:{}", conv_id)), Some(ctx), ) .await; } /// 状态变更(context: from / to;to=published=审核通过,to=archived=归档) pub async fn record_status_change(&self, knowledge_id: &str, from: &str, to: &str) { let ctx = serde_json::json!({ "from": from, "to": to }).to_string(); self.fire(knowledge_id, EVENT_STATUS_CHANGED, None, Some(ctx)).await; } /// 内部:写入并吞错误(fire-and-forget,失败只 warn 不阻断主流程) async fn fire( &self, knowledge_id: &str, event_type: &str, source_ref: Option, context_json: Option, ) { if let Err(e) = self.record(knowledge_id, event_type, source_ref, context_json).await { tracing::warn!("知识生命线事件写入失败(非阻断) [{}]: {}", event_type, e); } } }