重构: 拆audit.rs第一批reason helper(strategy)

- audit.rs → audit/mod.rs(959→812行) + 新建 audit/reason.rs(167行)
- 抽离 reason helper: resolve_project_label/resolve_task_label/build_approval_reason(3 fn自成闭包, pub(super))
- re-export 调用方零变更(commands::ai::audit::* 路径透明, 4处调用方+mod re-export核验)
- F-09 batch2 per_conv 保留(本批只搬 reason helper, conv_id路由不动)
主代兜底: cargo check --workspace 0 + test 98 + grep mod reason/use印证
strategy: 单批1-2文件原子; audit/mod.rs余812行(后续批restore/audit_finalize/find_cached)
This commit is contained in:
2026-06-19 04:11:47 +08:00
parent f026880fa7
commit e0ffd98223
6 changed files with 498 additions and 372 deletions

View File

@@ -8,7 +8,7 @@ use tauri::{AppHandle, Emitter, State};
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
use df_ai::provider::ChatMessage;
use df_storage::crud::{AiToolExecutionRepo, ProjectRepo, TaskRepo};
use df_storage::crud::AiToolExecutionRepo;
use df_storage::db::Database;
use df_storage::models::AiToolExecutionRecord;
@@ -121,159 +121,12 @@ pub async fn list_tool_executions(
}
/// 查项目可读标签id → "「项目名」(id=x)",查不到回退友好提示,空 id 返回空串。
///
/// AR-3审批卡片需显示对象名而非裸 id用户反馈"只返回 ID 不知道是什么数据")。
/// 三臂区分:
/// - Ok(Some) → 项目名标签
/// - Ok(None) → "项目已不存在"(真不存在,项目已彻底清除/外部 id
/// - Err → 裸 id 回退 + warn 日志DB 故障/锁/连接断,不误报"已不存在"误导用户)
async fn resolve_project_label(db: &Arc<Database>, 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)
}
}
}
// reason 拼装(resolve_project_label / resolve_task_label / build_approval_reason)
// 拆至子模块 audit/reason.rs(本批 helper 抽离,行为零变更)。
mod reason;
/// 查任务可读标签: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 分流。
async fn resolve_task_label(db: &Arc<Database>, 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 → namecreate_idea → title
/// - run_workflow → name
/// args 无可读字段或工具未特化时fallback 原 risk 模板(含风险等级提示)。
async fn build_approval_reason(
tool_name: &str,
args: &serde_json::Value,
risk_level: RiskLevel,
db: &Arc<Database>,
) -> 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::tool_registry::tool_display_hint(tool_name) {
// 轻量命中:直接用中文动作前缀 + 风险后缀(不含参数细节)
hint.to_string()
} else if let Some((template, keys)) = 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),
}
}
}
// process_tool_calls 调用 build_approval_reason,从子模块引入(super 可见,fn 为 pub(super))
use reason::build_approval_reason;
/// 启动恢复:从审计表重建 pending_approvals(重启前未审批的工具调用,内存态已丢)
///

View File

@@ -0,0 +1,167 @@
//! 审批 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<Database>, 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<Database>, 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 → namecreate_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<Database>,
) -> 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),
}
}
}