重构: Coordinator 接入 agentic loop + audit 拆分(approval/record)

- audit/approval.rs: 审批门控(风险分类+自动执行+挂起审批)

- audit/record.rs: 审计记录写入+历史查询

- coordinator.rs: 移除 deprecated,正式作为 P0 骨架可用

- run_agentic_loop: plan_execution_enabled 时 Coordinator 分解意图→记录 Plan

- audit/mod.rs 从 ~500 行精简至 ~200 行(编排层)
This commit is contained in:
lxy
2026-07-01 14:29:34 +08:00
parent a9ee9b1f74
commit fa410e6843
11 changed files with 1234 additions and 523 deletions
+162
View File
@@ -0,0 +1,162 @@
//! 审计记录写/查操作 + 审批历史 DTO + 审计面板查询 IPC。
//!
//! 第六批从 audit/mod.rs 抽离,行为零变更。包含:
//! - `audit_tool_call`:写一条工具执行审计记录(insert)
//! - `record_audit``audit_tool_call` 的别名,语义更清晰("记一条审计")
//! - `query_audit_history`:按条件查询审计历史记录
//! - `ToolExecutionDto`:传给前端的精简审计视图
//! - `list_tool_executions`:审批历史面板查询 IPC
//!
//! 依赖 audit/utils.rs 的 `truncate_chars` 做参数/结果截断,通过 `super::truncate_chars` 引用。
use serde::Serialize;
use tauri::State;
use df_ai::ai_tools::RiskLevel;
use df_storage::crud::AiToolExecutionRepo;
use df_storage::models::AiToolExecutionRecord;
use df_types::types::new_id;
use crate::commands::err_str;
use crate::commands::now_millis;
use crate::state::AppState;
use super::risk_str;
/// 写一条工具执行审计记录(insert 失败不阻断主流程,故 `let _ =`)
///
/// `decided_by` 有值(auto/human= 已决策执行 → 记 executed_at
/// `None`pending 待审批)→ executed_at 留空,待 audit_finalize 回填。
pub(crate) async fn audit_tool_call(
repo: &AiToolExecutionRepo,
conv_id: &str,
tool_call_id: &str,
tool_name: &str,
arguments: &str,
status: &str,
risk_level: RiskLevel,
result: Option<String>,
decided_by: Option<&str>,
message_id: Option<&str>,
) {
let executed_at = if decided_by.is_some() { Some(now_millis()) } else { None };
if let Err(e) = repo
.insert(AiToolExecutionRecord {
id: new_id(),
conversation_id: Some(conv_id.to_string()),
// F-260619-04 P1 消息级溯源:message_id 由调用方(process_tool_calls)从
// ContextManager 取当前 assistant 消息 id 传入(LLM 返回带 tool_calls 的
// assistant 消息已 push 到 per_conv.messages,入口取末条 assistant id)。
// None 表示无 assistant 消息(异常路径/老数据无 id),展示侧兼容。
message_id: message_id.map(|s| s.to_string()),
tool_call_id: tool_call_id.to_string(),
tool_name: tool_name.to_string(),
arguments: arguments.to_string(),
result,
status: status.to_string(),
risk_level: risk_str(risk_level).to_string(),
requested_at: now_millis(),
executed_at,
decided_by: decided_by.map(|s| s.to_string()),
})
.await
{
tracing::error!(
"audit_tool_call: 写审计记录失败(conv={}, tool_call_id={}, tool={}): {}",
conv_id,
tool_call_id,
tool_name,
e
);
}
}
/// `audit_tool_call` 的语义别名,功能完全相同。
/// 命名更符合"记录一条审计"的调用意图,供新代码使用。
pub(crate) async fn record_audit(
repo: &AiToolExecutionRepo,
conv_id: &str,
tool_call_id: &str,
tool_name: &str,
arguments: &str,
status: &str,
risk_level: RiskLevel,
result: Option<String>,
decided_by: Option<&str>,
message_id: Option<&str>,
) {
audit_tool_call(repo, conv_id, tool_call_id, tool_name, arguments, status, risk_level, result, decided_by, message_id).await;
}
/// 查询审计历史记录(分页,按 requested_at 倒序)。
///
/// 封装 `list_recent` 添加一层可读语义,方便未来扩展筛选条件。
pub(crate) async fn query_audit_history(
repo: &AiToolExecutionRepo,
limit: u32,
offset: u32,
) -> Result<Vec<AiToolExecutionRecord>, String> {
let limit = limit.min(200);
repo.list_recent(limit, offset)
.await
.map_err(|e| format!("query_audit_history 查询失败: {}", e))
}
/// 审批历史 DTO(传给前端的精简视图,敏感字段截断防泄露)
///
/// arguments/result 在落库时是完整 JSON(可能含项目名/路径/长结果),审计面板只展示摘要,
/// 故截断到固定长度(参数 120 / 结果 160),既保留可读性又不泄露全量数据到前端 DOM。
#[derive(Debug, Clone, Serialize)]
pub struct ToolExecutionDto {
pub id: String,
pub conversation_id: Option<String>,
pub tool_call_id: String,
pub tool_name: String,
/// 参数摘要(截断 120 字符,完整原值仍留库)
pub arguments_brief: String,
/// 结果摘要(截断 160 字符,None → 空串便于前端展示)
pub result_brief: Option<String>,
/// pending/approved/rejected/executing/completed/failed
pub status: String,
/// low/medium/high
pub risk_level: String,
pub requested_at: String,
pub executed_at: Option<String>,
/// human/autoNone 表示尚未决策
pub decided_by: Option<String>,
}
/// 审批历史面板查询:按 requested_at 倒序(最新在前)分页返回工具调用审计记录。
///
/// 默认 limit=50 / offset=0(第一页)。limit 在 storage 层钳制 ≤200 防滥用。
/// 敏感字段(arguments/result)截断成摘要返回,完整原值仍留库。
#[tauri::command]
pub async fn list_tool_executions(
state: State<'_, AppState>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<Vec<ToolExecutionDto>, String> {
let limit = limit.unwrap_or(50);
let offset = offset.unwrap_or(0);
let records = state
.ai_tool_executions
.list_recent(limit, offset)
.await
.map_err(err_str)?;
Ok(records
.into_iter()
.map(|r| ToolExecutionDto {
id: r.id,
conversation_id: r.conversation_id,
tool_call_id: r.tool_call_id,
tool_name: r.tool_name,
arguments_brief: super::truncate_chars(&r.arguments, 120),
result_brief: r.result.map(|s| super::truncate_chars(&s, 160)),
status: r.status,
risk_level: r.risk_level,
requested_at: r.requested_at,
executed_at: r.executed_at,
decided_by: r.decided_by,
})
.collect())
}