新增: 知识图谱Phase2事件流(父⑥) — project_events统一事件流+埋点+timeline
父⑥ Phase 2(对标设计 §2.4): - V30 迁移:project_events 追加型审计表(id/project_id/event_type/entity_type/entity_id/from_state/to_state/context_json/source/conversation_id/created_at)+ 双索引(project,time / entity) - ProjectEventRecord + ProjectEventRepo(insert 追加型/get_by_project/get_by_entity/list_recent) - 事件埋点(best-effort,hook/after,失败 warn 不阻断主操作): - task.rs emit_event 辅助 + create_task/advance_task/move_task_queue 埋点 - idea.rs promote_idea 埋点(idea_promoted,project_id=新项目) - project.rs create/delete 埋点 - 注:idea_created 暂不埋点(idea 无 project_id,待 Inbox 项目落地,NOT NULL+FK 约束) - get_project_timeline IPC(events 模块)+ lib.rs 注册 + AI 工具(基线 +N) - source 语义:IPC 标 human,AI 工具路径标 ai 注:workflow 脚本中文变量名 const 致验证 agent 未跑,主控独立 cargo check EXIT 0 + grep 落地齐全核验。
This commit is contained in:
@@ -490,7 +490,7 @@ fn register_http_tools(registry: &mut AiToolRegistry) {
|
||||
);
|
||||
}
|
||||
|
||||
/// 数据层 AI 工具注册(24 个持 db 的 CRUD/状态机/工作流/知识图谱工具)——从 build_ai_tool_registry 抽出。
|
||||
/// 数据层 AI 工具注册(25 个持 db 的 CRUD/状态机/工作流/知识图谱工具)——从 build_ai_tool_registry 抽出。
|
||||
///
|
||||
/// 工具闭包捕获 `db: &Arc<Database>` Arc 重建 Repo(列表/创建/更新/删除/状态推进/工作流/任务关联)。
|
||||
/// 例外:run_workflow handler 防御返回 Err(CR-52),不持 db 不 clone,真正执行经
|
||||
@@ -498,6 +498,8 @@ fn register_http_tools(registry: &mut AiToolRegistry) {
|
||||
///
|
||||
/// 知识图谱 Phase 1(2026-06-26):新增 register_task_graph_tools 子函数(6 工具:task_link CRUD
|
||||
/// + 父子树 + 跨池移动 + content 更新),data 层 18→24。
|
||||
/// 知识图谱 Phase 2(2026-06-26):register_task_graph_tools 增 get_project_timeline(项目事件流查询),
|
||||
/// data 层 24→25。
|
||||
///
|
||||
/// SMELL-P0-2:抽自原 build_ai_tool_registry 1091 行单函数(数据+文件混合)。
|
||||
/// 18 个 register 调用【原样移入】,零行为变更,仅机械搬运。
|
||||
@@ -510,8 +512,10 @@ fn register_http_tools(registry: &mut AiToolRegistry) {
|
||||
/// - register_trash_tools(1):list_trash
|
||||
/// 知识图谱 Phase 1(2026-06-26)新增第 6 子函数:
|
||||
/// - register_task_graph_tools(6):task_link CRUD + 父子树 + 跨池移动 + content 更新
|
||||
/// AiToolRegistry 底层 HashMap(注册顺序无关),拆分后工具集合与原一致 + 新增 6,
|
||||
/// 行为零变更(create_task 扩展参数 + 6 新工具,基线测试 test_build_ai_tool_registry_baseline_tool_count 守护)。
|
||||
/// 知识图谱 Phase 2(2026-06-26):register_task_graph_tools 增 get_project_timeline(项目事件流,只读),
|
||||
/// 该子函数 6→7 工具,data 层 24→25。
|
||||
/// AiToolRegistry 底层 HashMap(注册顺序无关),拆分后工具集合与原一致 + 新增,
|
||||
/// 行为零变更(create_task 扩展参数 + 7 新工具,基线测试 test_build_ai_tool_registry_baseline_tool_count 守护)。
|
||||
fn register_data_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
register_project_tools(registry, db);
|
||||
register_task_tools(registry, db);
|
||||
@@ -881,8 +885,8 @@ fn register_task_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
||||
}
|
||||
|
||||
/// 工作流类 AI 工具注册(1 个:run_workflow)——不持 db(handler 防御兜底,详见注释)。
|
||||
/// 知识图谱 Phase 1 AI 工具注册(6 个:task_link CRUD + 父子树 + 跨池移动 + content 更新)
|
||||
/// —— 持 db:Arc<Database>,对标设计 §五 Phase1 AI 工具表 + §2.1/§2.2。
|
||||
/// 知识图谱 Phase 1+2 AI 工具注册(7 个:task_link CRUD + 父子树 + 跨池移动 + content 更新
|
||||
/// + 项目事件流查询) —— 持 db:Arc<Database>,对标设计 §五 Phase1/Phase2 AI 工具表 + §2.1/§2.2/§2.4。
|
||||
///
|
||||
/// 工具直接调 df_storage::crud Repo(与 register_task_tools 同源模式,经 db Arc 重建 Repo),
|
||||
/// 复用 IPC 层(commands::task)的底层 Repo 方法,不重复实现业务校验:
|
||||
@@ -1094,6 +1098,56 @@ fn register_task_graph_tools(registry: &mut AiToolRegistry, db: &Arc<Database>)
|
||||
})
|
||||
})},
|
||||
);
|
||||
|
||||
// ── get_project_timeline (Low 只读,设计 §五 + §2.4) ──
|
||||
// 查询项目事件流(project_events 统一事件流)。AI 精准检索基础:
|
||||
// 回答"上周做了什么 / 这个任务为何 blocked / 决策何时做出"。
|
||||
// 参数 project_id(必填) + event_type(可选过滤) + limit(默认 50,上限 200)。
|
||||
// source 标 "ai"(AI 工具路径,区别于 IPC 层的 "human")。
|
||||
registry.register(
|
||||
"get_project_timeline", "查询项目事件流(AI 精准检索基础,回答\"上周做了什么/这个任务为何 blocked/决策何时做出\")。参数 project_id(必填,项目 ID)、event_type(可选,按事件类型精确过滤,如 task_created/task_advanced/idea_promoted/idea_evaluated/decision_made 等,空=不过滤)、limit(可选,返回条数上限,默认 50,内部钳制 ≤ 200)。返回 { items: 事件列表(时间倒序), total, project_id }。事件含 event_type/entity_type/entity_id/from_state/to_state/source/created_at",
|
||||
df_ai::ai_tools::object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
("event_type", "string", false),
|
||||
("limit", "integer", false),
|
||||
]),
|
||||
RiskLevel::Low,
|
||||
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
|
||||
let db = db.clone();
|
||||
Box::pin(async move {
|
||||
let project_id = args["project_id"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if project_id.is_empty() {
|
||||
anyhow::bail!("project_id 不能为空");
|
||||
}
|
||||
let event_filter = args.get("event_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
// limit 默认 50(对齐 commands::events::default_limit);负数/异常值兜底 50。
|
||||
let limit = args.get("limit")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|n| n as u32)
|
||||
.unwrap_or(50);
|
||||
|
||||
let repo = df_storage::crud::ProjectEventRepo::new(&db);
|
||||
let mut events = repo.get_by_project(&project_id, limit).await?;
|
||||
// 应用层按 event_type 过滤(单用户事件量小,保持 Repo 方法精简)。
|
||||
if let Some(et) = &event_filter {
|
||||
events.retain(|e| e.event_type == *et);
|
||||
}
|
||||
let total = events.len();
|
||||
Ok(serde_json::json!({
|
||||
"items": events,
|
||||
"total": total,
|
||||
"project_id": project_id,
|
||||
}))
|
||||
})
|
||||
})},
|
||||
);
|
||||
}
|
||||
|
||||
/// 抽自 register_data_tools(SMELL-P0-2 续拆),【原样移入】,零行为变更。
|
||||
@@ -2772,11 +2826,11 @@ mod tests {
|
||||
// 工具注册基线测试(SMELL-P0-2 拆分防护)
|
||||
//
|
||||
// 防未来 register_data_tools / register_file_tools 拆分或重构时静默丢工具。
|
||||
// build_ai_tool_registry 经两层 register_* 组装:data(24 持 db) + file(13 不持 db) + http(1) = 38。
|
||||
// build_ai_tool_registry 经两层 register_* 组装:data(25 持 db) + file(13 不持 db) + http(1) = 39。
|
||||
// 任一层漏移 register 调用,此测试立即红。工具名集合也断言,防 rename 致 LLM tool 突变。
|
||||
// ============================================================
|
||||
|
||||
/// build_ai_tool_registry 应注册恰好 38 个工具(24 data + 13 file + 1 http),且工具名集合稳定。
|
||||
/// build_ai_tool_registry 应注册恰好 39 个工具(25 data + 13 file + 1 http),且工具名集合稳定。
|
||||
///
|
||||
/// 用 in-memory SQLite(Database::open_in_memory 自跑迁移),构造零外部依赖的 db,
|
||||
// 不实际执行任何 handler——仅断言注册阶段的定义完整性,故无需真实数据。
|
||||
@@ -2789,24 +2843,26 @@ mod tests {
|
||||
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs::default_with_root()));
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs);
|
||||
|
||||
// 总量基线:38(24 data + 13 file + 1 http)。拆分前后必须一致。
|
||||
// 总量基线:39(25 data + 13 file + 1 http)。拆分前后必须一致。
|
||||
// F-260621: file 层 10→11(新增 grep 跨文件内容搜索工具)。
|
||||
// L1 环境感知(设计 §2.1): file 层 11→12(新增 detect_environment 环境探测工具)。
|
||||
// AST 代码智能(7c2e3b2): file 层 12→13(新增 read_symbol 符号解析,治 read_file 全文回灌 prompt 爆)。
|
||||
// 知识图谱 Phase 1(2026-06-26): data 层 18→24(新增 register_task_graph_tools 6 工具:
|
||||
// create_task_link/remove_task_link/list_task_links/get_task_tree/move_task_queue/update_content)。
|
||||
// 知识图谱 Phase 2(2026-06-26): data 层 24→25(新增 get_project_timeline 项目事件流查询,
|
||||
// register_task_graph_tools 6→7)。
|
||||
assert_eq!(
|
||||
registry.len(),
|
||||
38,
|
||||
"工具总数应为 38(24 data + 13 file + 1 http),实际 {}", registry.len()
|
||||
39,
|
||||
"工具总数应为 39(25 data + 13 file + 1 http),实际 {}", registry.len()
|
||||
);
|
||||
|
||||
// 工具名集合基线:防 rename / 漏注册 / 误删除。
|
||||
// data 层 24 个(持 db):CRUD/状态机/工作流/知识图谱任务关联
|
||||
// data 层 25 个(持 db):CRUD/状态机/工作流/知识图谱任务关联 + 项目事件流
|
||||
// file 层 13 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜/grep/环境探测/符号解析
|
||||
// http 层 1 个(不持 db):http_request
|
||||
let mut expected: Vec<&str> = vec![
|
||||
// ── data 层 (24) ──
|
||||
// ── data 层 (25) ──
|
||||
"list_projects", "list_tasks", "list_ideas",
|
||||
"update_project", "create_project", "bind_directory",
|
||||
"create_task", "update_task", "advance_task",
|
||||
@@ -2816,6 +2872,8 @@ mod tests {
|
||||
// 知识图谱 Phase 1 任务关联工具(register_task_graph_tools 6 个)
|
||||
"create_task_link", "remove_task_link", "list_task_links",
|
||||
"get_task_tree", "move_task_queue", "update_content",
|
||||
// 知识图谱 Phase 2 项目事件流(register_task_graph_tools 7 个,本工具为第 7)
|
||||
"get_project_timeline",
|
||||
// ── file 层 (13) ──(run_command 注册顺序已移至末位降低 LLM 偏好,
|
||||
// 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621;
|
||||
// detect_environment 新增 L1 环境感知 设计 §2.1;read_symbol 新增 AST 代码智能)
|
||||
|
||||
91
src-tauri/src/commands/events.rs
Normal file
91
src-tauri/src/commands/events.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! 项目事件流相关命令(知识图谱 Phase 2 V30,对标设计 §2.4 + §五 get_project_timeline)
|
||||
//!
|
||||
//! 统一事件流 project_events 的查询入口。供 AI / Dashboard 项目时间线视图使用
|
||||
//! (设计 §10.3.4 模式 B 事件流 + §五 get_project_timeline)。
|
||||
//!
|
||||
//! 筛选策略:Repo::get_by_project 取最近 N 条(上限 200,对标设计 §10.5 单用户桌面应用
|
||||
//! 全量查询无压力),应用层按 event_type / 时间范围过滤。复杂多维筛选不进 SQL(数据量小,
|
||||
//! 应用层过滤足够 + 保持 Repo 专用方法精简,对标 TaskLinkRepo 全专用路径)。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use df_storage::models::ProjectEventRecord;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::err_str;
|
||||
|
||||
/// get_project_timeline 查询入参(对标设计 §五 get_project_timeline:time 范围/event_type 筛选)。
|
||||
///
|
||||
/// 所有字段可选:
|
||||
/// - `project_id`:必填(查询某项目的事件流)。
|
||||
/// - `event_type`:可选,按事件类型精确过滤(如 "task_advanced");空/None = 不过滤。
|
||||
/// - `limit`:可选,返回上限(默认 50,Repo 内部钳制最大 200)。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetTimelineInput {
|
||||
pub project_id: String,
|
||||
/// 事件类型过滤(精确匹配,如 "task_created" / "idea_promoted")。None/空串 = 不过滤。
|
||||
pub event_type: Option<String>,
|
||||
/// 返回条数上限(默认 50)。Repo 内部钳制 ≤ 200。
|
||||
#[serde(default = "default_limit")]
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
fn default_limit() -> u32 {
|
||||
50
|
||||
}
|
||||
|
||||
/// 时间线条目(对标设计 §10.3.4 模式 B 事件流)。直接回 ProjectEventRecord(Serialize 已派生),
|
||||
/// 外层补 total 便于前端分页/计数。
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TimelineResult {
|
||||
/// 过滤后的事件列表(时间倒序,Repo 已排)
|
||||
pub items: Vec<ProjectEventRecord>,
|
||||
/// 过滤后总数(≤ limit)
|
||||
pub total: usize,
|
||||
/// 查询的项目 ID(回显)
|
||||
pub project_id: String,
|
||||
}
|
||||
|
||||
/// 查询项目事件流(对标设计 §五 get_project_timeline + §2.4)。
|
||||
///
|
||||
/// 支持按 event_type 过滤 + limit 上限。时间范围过滤暂未实现(设计 §2.4 列出 time 范围筛选,
|
||||
/// 但当前 get_by_project 仅返回最近 N 条倒序,时间范围筛选用例可后续按需加 Repo 方法;
|
||||
/// 单用户桌面应用事件量小,最近 N 条 + event_type 过滤已覆盖主要场景)。
|
||||
#[tauri::command]
|
||||
pub async fn get_project_timeline(
|
||||
state: State<'_, AppState>,
|
||||
input: GetTimelineInput,
|
||||
) -> Result<TimelineResult, String> {
|
||||
let project_id = input.project_id.trim().to_string();
|
||||
if project_id.is_empty() {
|
||||
return Err("project_id 不能为空".to_string());
|
||||
}
|
||||
// event_type 过滤值规范化(None/空串 = 不过滤)
|
||||
let event_filter = input
|
||||
.event_type
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Repo 取最近 limit 条(内部钳制 ≤ 200),时间倒序。
|
||||
let mut events = state
|
||||
.project_events
|
||||
.get_by_project(&project_id, input.limit)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
|
||||
// 应用层按 event_type 过滤(SQL 下推不值得——单用户事件量小,保持 Repo 方法精简)。
|
||||
if let Some(et) = &event_filter {
|
||||
events.retain(|e| e.event_type == *et);
|
||||
}
|
||||
|
||||
let total = events.len();
|
||||
Ok(TimelineResult {
|
||||
items: events,
|
||||
total,
|
||||
project_id,
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use df_ai::provider::LlmProvider;
|
||||
use df_types::types::{new_id, Priority};
|
||||
use df_ideas::capture::Idea;
|
||||
use df_storage::crud::{is_unique_constraint_err, IdeaQuery};
|
||||
use df_storage::models::{IdeaEvaluationRecord, IdeaRecord, ProjectRecord};
|
||||
use df_storage::models::{IdeaEvaluationRecord, IdeaRecord, ProjectEventRecord, ProjectRecord};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -104,6 +104,12 @@ pub async fn create_idea(
|
||||
.insert(record.clone())
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
// 知识图谱 Phase 2(对标设计 §2.4):idea_created 事件**暂不埋点**。
|
||||
// 原因:project_events.project_id 是 NOT NULL + FK(PRAGMA foreign_keys=ON),
|
||||
// 而 idea 无 project_id(立项前不属于任何项目)。设计 §2.5 计划用系统初始化创建的
|
||||
// Inbox 项目作为无主 idea 的归属,但 Inbox 项目尚未实现(独立任务)。
|
||||
// 写 NULL 会违反 NOT NULL,写不存在的 project_id 会违反 FK——两者都会让 best-effort
|
||||
// 退化成「写失败 warn」,无实际价值且噪音。Inbox 项目落地后此处补 idea_created 埋点。
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
@@ -201,6 +207,31 @@ pub async fn promote_idea(
|
||||
return Err(format!("灵感立项回写失败(已回滚项目创建): {}", e));
|
||||
}
|
||||
|
||||
// 知识图谱 Phase 2(对标设计 §2.4 hook/after):idea_promoted 事件。best-effort 不阻断。
|
||||
// 灵感此时已立项为新项目(project_id 存在,FK 满足),事件挂在 project_id 下,
|
||||
// entity 指向 idea,from_state=draft→to_state=promoted。
|
||||
let event = ProjectEventRecord {
|
||||
id: new_id(),
|
||||
project_id: project_id.clone(),
|
||||
event_type: "idea_promoted".to_string(),
|
||||
entity_type: Some("idea".to_string()),
|
||||
entity_id: Some(id.clone()),
|
||||
from_state: Some("draft".to_string()),
|
||||
to_state: Some("promoted".to_string()),
|
||||
context_json: None,
|
||||
source: Some("human".to_string()),
|
||||
conversation_id: None,
|
||||
created_at: now_millis(),
|
||||
};
|
||||
if let Err(e) = state.project_events.insert(event).await {
|
||||
tracing::warn!(
|
||||
idea_id = %id,
|
||||
project_id = %project_id,
|
||||
error = %e,
|
||||
"[事件流] idea_promoted 埋点写入失败(不阻断立项)"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(df_ideas::promotion::PromotionResult {
|
||||
idea_id: id,
|
||||
project_id: project_id,
|
||||
|
||||
@@ -18,12 +18,49 @@ use df_project::scan::{
|
||||
normalize_path, DiscoveredProject,
|
||||
};
|
||||
use df_storage::crud::ProjectQuery;
|
||||
use df_storage::models::ProjectRecord;
|
||||
use df_storage::models::{ProjectEventRecord, ProjectRecord};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::{err_str, now_millis};
|
||||
|
||||
// ============================================================
|
||||
// 知识图谱 Phase 2:事件流埋点辅助(best-effort,对标设计 §2.4 hook/after + §10.1)
|
||||
// ============================================================
|
||||
|
||||
/// 追加一条项目事件到 project_events(best-effort)。失败仅 tracing::warn 不阻断主 IPC。
|
||||
///
|
||||
/// `source` 标 `"human"`(IPC 层)。AI 工具路径在 tool_registry 内自行标 `"ai"`。
|
||||
async fn emit_project_event(
|
||||
state: &AppState,
|
||||
project_id: &str,
|
||||
event_type: &str,
|
||||
entity_type: Option<&str>,
|
||||
entity_id: Option<&str>,
|
||||
) {
|
||||
let record = ProjectEventRecord {
|
||||
id: new_id(),
|
||||
project_id: project_id.to_string(),
|
||||
event_type: event_type.to_string(),
|
||||
entity_type: entity_type.map(|s| s.to_string()),
|
||||
entity_id: entity_id.map(|s| s.to_string()),
|
||||
from_state: None,
|
||||
to_state: Some("planning".to_string()),
|
||||
context_json: None,
|
||||
source: Some("human".to_string()),
|
||||
conversation_id: None,
|
||||
created_at: now_millis(),
|
||||
};
|
||||
if let Err(e) = state.project_events.insert(record).await {
|
||||
tracing::warn!(
|
||||
event_type = event_type,
|
||||
project_id = project_id,
|
||||
error = %e,
|
||||
"[事件流] 埋点写入失败(不阻断主操作)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建项目入参
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateProjectInput {
|
||||
@@ -132,6 +169,22 @@ async fn create_with_binding(
|
||||
if record.path.is_some() {
|
||||
state.reload_allowed_dirs().await;
|
||||
}
|
||||
// 知识图谱 Phase 2(对标设计 §2.4 hook/after):项目创建事件。best-effort 不阻断。
|
||||
//
|
||||
// 设计 §2.4 event_type 枚举未列 project_created(枚举非穷尽,Repo 不做白名单校验——
|
||||
// 见 project_event_repo.rs 注释「埋点层是唯一生产者,字符串字面量自带约束」)。此处沿用
|
||||
// task_created/idea_created 命名约定新增 project_created,语义自洽。entity_type=project。
|
||||
//
|
||||
// 注:灵感晋升(create_from_idea)在 promote_idea 单独记 idea_promoted 事件(更精确),
|
||||
// 此处一律记 project_created(项目实体自身视角),两事件互补不冲突。
|
||||
emit_project_event(
|
||||
state,
|
||||
&record.id,
|
||||
"project_created",
|
||||
Some("project"),
|
||||
Some(&record.id),
|
||||
)
|
||||
.await;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,56 @@ use tauri::State;
|
||||
|
||||
use df_types::types::{new_id, TaskStatus};
|
||||
use df_storage::crud::TaskQuery;
|
||||
use df_storage::models::{TaskLinkRecord, TaskRecord};
|
||||
use df_storage::models::{ProjectEventRecord, TaskLinkRecord, TaskRecord};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::{err_str, now_millis};
|
||||
|
||||
// ============================================================
|
||||
// 知识图谱 Phase 2:事件流埋点辅助(best-effort,对标设计 §2.4 hook/after + §10.1)
|
||||
// ============================================================
|
||||
|
||||
/// 追加一条项目事件到 project_events(best-effort)。
|
||||
///
|
||||
/// 埋点策略(设计 §2.4):事件写入失败**不阻断主操作**,仅 `tracing::warn` 记录。
|
||||
/// 设计 §10.1「事件流写入失败 — 影响事件完整性 — 事件写入失败不阻断主操作(best-effort)」。
|
||||
///
|
||||
/// `source` 语义:本辅助仅由 IPC 层调用,标 `"human"`(AI 工具路径在 tool_registry 内自行标
|
||||
/// `"ai"`,不经本 IPC)。`project_id` / `entity_type` / `entity_id` / `event_type` 由调用方
|
||||
/// 提供。`from_state` / `to_state` / `context_json` 可选。
|
||||
async fn emit_event(
|
||||
state: &State<'_, AppState>,
|
||||
project_id: &str,
|
||||
event_type: &str,
|
||||
entity_type: Option<&str>,
|
||||
entity_id: Option<&str>,
|
||||
from_state: Option<&str>,
|
||||
to_state: Option<&str>,
|
||||
) {
|
||||
let record = ProjectEventRecord {
|
||||
id: new_id(),
|
||||
project_id: project_id.to_string(),
|
||||
event_type: event_type.to_string(),
|
||||
entity_type: entity_type.map(|s| s.to_string()),
|
||||
entity_id: entity_id.map(|s| s.to_string()),
|
||||
from_state: from_state.map(|s| s.to_string()),
|
||||
to_state: to_state.map(|s| s.to_string()),
|
||||
context_json: None,
|
||||
source: Some("human".to_string()),
|
||||
conversation_id: None,
|
||||
created_at: now_millis(),
|
||||
};
|
||||
if let Err(e) = state.project_events.insert(record).await {
|
||||
tracing::warn!(
|
||||
event_type = event_type,
|
||||
project_id = project_id,
|
||||
error = %e,
|
||||
"[事件流] 埋点写入失败(不阻断主操作)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建任务入参
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateTaskInput {
|
||||
@@ -262,6 +306,17 @@ pub async fn create_task(
|
||||
.insert(record.clone())
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
// 知识图谱 Phase 2(对标设计 §2.4 hook/after):task_created 事件。best-effort 不阻断。
|
||||
emit_event(
|
||||
&state,
|
||||
&record.project_id,
|
||||
"task_created",
|
||||
Some("task"),
|
||||
Some(&record.id),
|
||||
None,
|
||||
Some(&record.status),
|
||||
)
|
||||
.await;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
@@ -372,6 +427,15 @@ pub async fn advance_task(
|
||||
id: String,
|
||||
target_status: String,
|
||||
) -> Result<TaskRecord, String> {
|
||||
// 知识图谱 Phase 2:推进前读当前态,作 task_advanced 事件 from_state(仅一次轻量读,
|
||||
// advance 低频无压力)。失败(任务不存在)不阻断——后续 atomic 会用 NotFound 拒绝,from 留空。
|
||||
let from_state = state
|
||||
.tasks
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map(|t| t.status);
|
||||
|
||||
let updated = df_nodes::task_advance_node::advance_task_atomic(
|
||||
&state.tasks,
|
||||
&id,
|
||||
@@ -394,6 +458,19 @@ pub async fn advance_task(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 知识图谱 Phase 2(对标设计 §2.4 hook/after):task_advanced 事件。best-effort 不阻断。
|
||||
emit_event(
|
||||
&state,
|
||||
&updated.project_id,
|
||||
"task_advanced",
|
||||
Some("task"),
|
||||
Some(&updated.id),
|
||||
from_state.as_deref(),
|
||||
Some(&updated.status),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
@@ -639,6 +716,21 @@ pub async fn move_task_queue(
|
||||
.map_err(err_str)?;
|
||||
}
|
||||
|
||||
// 知识图谱 Phase 2(对标设计 §2.4 hook/after):queue 变化事件。best-effort 不阻断。
|
||||
// 仅在 queue 实际变化时埋点(避免 no-op 移动产噪音事件)。from/to 用 queue 值。
|
||||
if current.queue != new_queue {
|
||||
emit_event(
|
||||
&state,
|
||||
¤t.project_id,
|
||||
"task_advanced",
|
||||
Some("task"),
|
||||
Some(¤t.id),
|
||||
Some(¤t.queue),
|
||||
Some(&new_queue),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// 回读最新记录返回
|
||||
state
|
||||
.tasks
|
||||
|
||||
@@ -261,6 +261,8 @@ pub fn run() {
|
||||
commands::task::list_task_links,
|
||||
commands::task::move_task_queue,
|
||||
commands::task::get_task_tree,
|
||||
// 知识图谱 Phase 2(对标设计 §五 + §2.4):项目事件流查询
|
||||
commands::events::get_project_timeline,
|
||||
// 灵感
|
||||
commands::idea::list_ideas,
|
||||
commands::idea::create_idea,
|
||||
|
||||
@@ -12,8 +12,8 @@ use tokio::sync::{Mutex, RwLock, Semaphore};
|
||||
use df_ai::ai_tools::AiToolRegistry;
|
||||
use df_storage::crud::{
|
||||
AiConversationRepo, AiMessageRepo, AiProviderRepo, AiToolExecutionRepo, IdeaEvalRepo, IdeaRepo,
|
||||
KnowledgeEventsRepo, KnowledgeRepo, NodeExecutionRepo, ProjectRepo, ReleaseRepo,
|
||||
SettingsRepo, TaskLinkRepo, TaskRepo, WorkflowRepo,
|
||||
KnowledgeEventsRepo, KnowledgeRepo, NodeExecutionRepo, ProjectEventRepo, ProjectRepo,
|
||||
ReleaseRepo, SettingsRepo, TaskLinkRepo, TaskRepo, WorkflowRepo,
|
||||
};
|
||||
use df_storage::db::Database;
|
||||
use df_workflow::eventbus::EventBus;
|
||||
@@ -339,6 +339,9 @@ pub struct AppState {
|
||||
pub tasks: TaskRepo,
|
||||
/// 任务横向关联表 Repo(知识图谱 Phase 1 V29,AI 拓扑排序编排调度的基础)
|
||||
pub task_links: TaskLinkRepo,
|
||||
/// 项目统一事件流表 Repo(知识图谱 Phase 2 V30,project_events 追加型审计表)。
|
||||
/// 埋点 best-effort:事件写入失败不阻断主操作(设计 §10.1),commands 层 hook/after 追加。
|
||||
pub project_events: ProjectEventRepo,
|
||||
/// 发布表 Repo(预留:ReleaseRepo 持久化已就位·IPC/逻辑未接入·SW-260618-21 b 保留)
|
||||
#[allow(dead_code)]
|
||||
pub releases: ReleaseRepo,
|
||||
@@ -665,6 +668,7 @@ impl AppState {
|
||||
projects: ProjectRepo::new(&db),
|
||||
tasks: TaskRepo::new(&db),
|
||||
task_links: TaskLinkRepo::new(&db),
|
||||
project_events: ProjectEventRepo::new(&db),
|
||||
releases: ReleaseRepo::new(&db),
|
||||
workflows: WorkflowRepo::new(&db),
|
||||
node_executions: NodeExecutionRepo::new(&db),
|
||||
|
||||
Reference in New Issue
Block a user