//! 审批 reason 拼装:工具调用 → 可读审批文案(含项目/任务 label 解析)。 //! //! 拆自 audit.rs(本批 helper 抽离,行为零变更): //! - `resolve_project_label` / `resolve_task_label`:id → 可读标签(三臂分流) //! - `build_approval_reason`:按工具名 + args 拼审批 reason //! //! 仅被 `process_tool_calls`(audit/mod.rs)调用,无外部调用方。 use std::sync::Arc; use df_ai::ai_tools::RiskLevel; use df_storage::crud::{ProjectRepo, TaskRepo}; use df_storage::db::Database; /// 查项目可读标签:id → "「项目名」(id=x)"。 /// /// AR-3:审批卡片需显示对象名而非裸 id(用户反馈"只返回 ID 不知道是什么数据")。 /// 三臂区分: /// - Ok(Some) → 项目名标签 /// - Ok(None) → "项目已不存在"(真不存在,项目已彻底清除/外部 id) /// - Err → 裸 id 回退 + warn 日志(DB 故障/锁/连接断,不误报"已不存在"误导用户) pub(super) async fn resolve_project_label(db: &Arc, id: &str) -> String { if id.is_empty() { return String::new(); } let repo = ProjectRepo::new(db); match repo.get_by_id(id).await { Ok(Some(p)) => format!("「{}」(id={})", p.name, id), Ok(None) => format!("(项目已不存在, id={})", id), Err(e) => { tracing::warn!("resolve_project_label: 查询项目 id={} 失败,回退裸 id: {}", id, e); format!("(id={})", id) } } } /// 查任务可读标签:id → "「任务标题」(id=x)",对齐 resolve_project_label 三臂语义。 /// /// UX-260618-14:advance_task 审批卡的 id 是 task_id,原 build_approval_reason 把 "id" /// 统一走 resolve_project_label(查 projects 表),误把任务 id 当项目 id 解析,永远落到 /// "项目已不存在"。本方法改查 tasks 表,与 resolve_project_label 同 Ok(None)/Err 分流。 pub(super) async fn resolve_task_label(db: &Arc, id: &str) -> String { if id.is_empty() { return String::new(); } let repo = TaskRepo::new(db); match repo.get_by_id(id).await { Ok(Some(t)) => format!("「{}」(id={})", t.title, id), Ok(None) => format!("(任务已不存在, id={})", id), Err(e) => { tracing::warn!("resolve_task_label: 查询任务 id={} 失败,回退裸 id: {}", id, e); format!("(id={})", id) } } } /// 拼审批 reason:按工具名 + args 取可读字段,让用户知道"审批要做什么"。 /// /// 主要审批工具特化(带对象名/标识): /// - delete_project/restore_project/purge_project → 查项目名拼"删除项目「名」(id=x)" /// - update_project → 查项目名 + field /// - bind_directory → path + 查项目名 /// - create_task → title + 查 project_id 项目名;create_project → name;create_idea → title /// - run_workflow → name /// args 无可读字段或工具未特化时,fallback 原 risk 模板(含风险等级提示)。 pub(super) async fn build_approval_reason( tool_name: &str, args: &serde_json::Value, risk_level: RiskLevel, db: &Arc, ) -> String { let s = |key: &str| args.get(key).and_then(|v| v.as_str()).unwrap_or(""); // 优先级:tool_display_hint(轻量标签) → display_hint_for_tool(模板填充) → 硬编码 fallback let detail = if let Some(hint) = super::super::tool_registry::tool_display_hint(tool_name) { // 轻量命中:直接用中文动作前缀 + 风险后缀(不含参数细节) hint.to_string() } else if let Some((template, keys)) = super::super::tool_registry::display_hint_for_tool(tool_name) { // 按 keys 列表取参数值;含 "id"/"project_id" 的值走 resolve_project_label 解析 let mut values = Vec::with_capacity(keys.len()); for &key in keys { let val = s(key); if val.is_empty() { values.clear(); break; } match key { // advance_task 的 id 是 task_id(非项目 id),改查 tasks 表,避免误报"项目已不存在" "id" if tool_name == "advance_task" => values.push(resolve_task_label(db, &val).await), "id" | "project_id" => values.push(resolve_project_label(db, &val).await), _ => values.push(val.to_string()), } } // 任一关键参数为空则整条 fallback 为空串(与原行为一致) if values.is_empty() { String::new() } else { // 模板 {} 占位符按位置填充 let mut result = template.to_string(); for v in &values { result = result.replacen("{}", v, 1); } result } } else { // fallback:原有完整硬编码逻辑(保持行为不变) match tool_name { "delete_project" => { let id = s("id"); if !id.is_empty() { format!("删除项目{}", resolve_project_label(db, id).await) } else { String::new() } } "restore_project" => { let id = s("id"); if !id.is_empty() { format!("从回收站恢复项目{}", resolve_project_label(db, id).await) } else { String::new() } } "purge_project" => { let id = s("id"); if !id.is_empty() { format!("永久删除项目及关联数据,不可恢复{}", resolve_project_label(db, id).await) } else { String::new() } } "update_project" => { let id = s("id"); let field = s("field"); if !id.is_empty() { format!("修改项目{}字段「{}」", resolve_project_label(db, id).await, field) } else if !field.is_empty() { format!("修改项目字段「{}」", field) } else { String::new() } } "bind_directory" => { let id = s("id"); let path = s("path"); if !path.is_empty() && !id.is_empty() { format!("绑定目录:{}(项目{})", path, resolve_project_label(db, id).await) } else if !path.is_empty() { format!("绑定目录:{}", path) } else { String::new() } } "create_task" => { let title = s("title"); let pid = s("project_id"); if !title.is_empty() && !pid.is_empty() { format!("创建任务:{}(项目{})", title, resolve_project_label(db, pid).await) } else if !title.is_empty() { format!("创建任务:{}", title) } else { String::new() } } "create_project" => { let name = s("name"); if !name.is_empty() { format!("创建项目:「{}」", name) } else { String::new() } } "create_idea" => { let title = s("title"); if !title.is_empty() { format!("捕获灵感:{}", title) } else { String::new() } } _ => String::new(), } }; if detail.is_empty() { // fallback:无可读字段,保留风险等级提示模板 match risk_level { RiskLevel::High => "高风险操作,必须人工批准".to_string(), _ => "创建操作,请确认是否执行".to_string(), } } else { // 拼上风险等级后缀(高风险标注,便于用户权衡) match risk_level { RiskLevel::High => format!("{}(高风险,需人工批准)", detail), _ => format!("{}(请确认是否执行)", detail), } } }