新增: Phase2 阶段收尾(Sprint 1-20)

重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录
功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore
修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸
文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新

详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
2026-06-14 14:08:20 +08:00
parent 98393b4908
commit cf017f81e2
167 changed files with 19549 additions and 6886 deletions

View File

@@ -0,0 +1,123 @@
//! 知识生命线记录器 — 统一入口,各业务流程只调一行
//!
//! 设计目标:
//! - 事件记录逻辑不散落在各业务流程内部(提取/注入/状态变更),集中于此模块
//! - fire-and-forget 友好:便捷方法内部吞掉错误(只 warn),调用方无需处理 Result
//! - 易扩展:新事件类型只加一个便捷方法 + 一行 context 构造,不改主流程
use std::sync::Arc;
use df_core::types::new_id;
use df_storage::crud::KnowledgeEventsRepo;
use df_storage::db::Database;
use df_storage::models::KnowledgeEventRecord;
use super::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<Database>,
}
impl KnowledgeTimeline {
pub fn new(db: &Arc<Database>) -> Self {
Self { db: db.clone() }
}
/// 通用写入(底层入口,便捷方法均经此)
pub async fn record(
&self,
knowledge_id: &str,
event_type: &str,
source_ref: Option<String>,
context_json: Option<String>,
) -> 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(|e| e.to_string())?;
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<String>,
context_json: Option<String>,
) {
if let Err(e) = self.record(knowledge_id, event_type, source_ref, context_json).await {
tracing::warn!("知识生命线事件写入失败(非阻断) [{}]: {}", event_type, e);
}
}
}