diff --git a/src-tauri/src/commands/ai/audit/diff.rs b/src-tauri/src/commands/ai/audit/diff.rs new file mode 100644 index 0000000..f8273c5 --- /dev/null +++ b/src-tauri/src/commands/ai/audit/diff.rs @@ -0,0 +1,29 @@ +//! AE-2025-03(路径 B):write_file 审批预览 diff 生成。 +//! +//! 第五批从 audit/mod.rs 抽离,行为零变更。`build_write_file_diff` 仅 process_tool_calls +//! 在 write_file 高风险审批分支裸名调用,依赖 `super::super::tool_registry::generate_diff` +//! (F-260615-10 LCS 行级 diff),故 diff.rs 直接 `use super::super::tool_registry::generate_diff`。 +//! +//! re-export 保持原路径透明:audit/mod.rs 通过 `use diff::build_write_file_diff` 裸名调用。 + +use super::super::tool_registry::generate_diff; + +/// 从 write_file args 取 path(旧文件路径)+ content(新内容), +/// 预读旧文件 → 复用 `generate_diff`(F-260615-10 LCS 行级 diff)注入审批事件。 +/// 旧文件不存在(新建)/ 读失败 / args 缺字段 → None(前端回退显新 content)。 +/// +/// **仅读不改**:审批未通过前不动文件;读路径不校验(write_file handler 自身会校验 +/// validate_path,审批拒绝则 handler 不执行,恶意路径读失败仅得到 None 不产生危害)。 +pub(super) async fn build_write_file_diff(args: &serde_json::Value) -> Option { + let path = args.get("path")?.as_str()?; + let new_content = args.get("content")?.as_str()?; + match tokio::fs::read_to_string(path).await { + Ok(old_content) => { + if old_content == new_content { + return None; // 无变化不生成 diff(理论上 write_file 不会如此,容错) + } + Some(generate_diff(&old_content, new_content)) + } + Err(_) => None, // 文件不存在(新建)/ 无读权限 → 无 diff,前端显新 content + } +} diff --git a/src-tauri/src/commands/ai/audit/mod.rs b/src-tauri/src/commands/ai/audit/mod.rs index 653074a..0968cbc 100644 --- a/src-tauri/src/commands/ai/audit/mod.rs +++ b/src-tauri/src/commands/ai/audit/mod.rs @@ -16,26 +16,21 @@ use crate::state::AppState; use crate::commands::err_str; use super::{AiChatEvent, AiSession, PendingApproval, ToolCallDraft}; -use super::tool_registry::generate_diff; +// tool_registry::generate_diff 经 audit/diff.rs 直接 use(build_write_file_diff 内调用), +// 本文件不再裸名用 generate_diff,故不再在此 use(避免 unused import warning)。 -/// RiskLevel → 审计记录字符串(low/medium/high) -pub(crate) fn risk_str(r: RiskLevel) -> &'static str { - match r { - RiskLevel::Low => "low", - RiskLevel::Medium => "medium", - RiskLevel::High => "high", - } -} +// utils(audit/utils.rs):RiskLevel ↔ 字符串转换 + 字符串安全截断纯 helper。 +// 第五批从本文件抽离,行为零变更。pub(crate) use 保持 finalize / restore 子模块 +// super::risk_str / super::risk_from_str 引用路径不变;truncate_chars 仅供本文件 DTO 装配裸名用。 +mod utils; +pub(crate) use utils::{risk_from_str, risk_str}; +use utils::truncate_chars; -/// 审计记录字符串 → RiskLevel(启动重建 pending_approvals 用,未知串返回 None 跳过) -pub(crate) fn risk_from_str(s: &str) -> Option { - match s { - "low" => Some(RiskLevel::Low), - "medium" => Some(RiskLevel::Medium), - "high" => Some(RiskLevel::High), - _ => None, - } -} +// diff(audit/diff.rs):AE-2025-03 write_file 审批预览 diff 生成。 +// 第五批从本文件抽离,行为零变更。use 保持本文件 process_tool_calls 裸名调用 build_write_file_diff。 +// generate_diff 经 `use super::generate_diff` 引入供 diff.rs 复用。 +mod diff; +use diff::build_write_file_diff; // ============================================================ // 审批历史查询 IPC(AE-2025-08) @@ -65,16 +60,6 @@ pub struct ToolExecutionDto { pub decided_by: Option, } -/// 截断字符串到 max 字符(按 char_indices 边界切,避免切坏中文/emoji) -fn truncate_chars(s: &str, max: usize) -> String { - if s.chars().count() <= max { - return s.to_string(); - } - let mut out: String = s.chars().take(max).collect(); - out.push('…'); - out -} - /// 审批历史面板查询:按 requested_at 倒序(最新在前)分页返回工具调用审计记录。 /// /// 默认 limit=50 / offset=0(第一页)。limit 在 storage 层钳制 ≤200 防滥用。 @@ -141,28 +126,6 @@ pub(super) use cache::{find_cached_high_risk_result, PENDING_APPROVAL_PLACEHOLDE mod data_change; pub(crate) use data_change::emit_data_changed; -/// AE-2025-03(路径 B):write_file 审批预览 diff 生成。 -/// -/// 从 write_file args 取 path(旧文件路径)+ content(新内容), -/// 预读旧文件 → 复用 `generate_diff`(F-260615-10 LCS 行级 diff)注入审批事件。 -/// 旧文件不存在(新建)/ 读失败 / args 缺字段 → None(前端回退显新 content)。 -/// -/// **仅读不改**:审批未通过前不动文件;读路径不校验(write_file handler 自身会校验 -/// validate_path,审批拒绝则 handler 不执行,恶意路径读失败仅得到 None 不产生危害)。 -async fn build_write_file_diff(args: &serde_json::Value) -> Option { - let path = args.get("path")?.as_str()?; - let new_content = args.get("content")?.as_str()?; - match tokio::fs::read_to_string(path).await { - Ok(old_content) => { - if old_content == new_content { - return None; // 无变化不生成 diff(理论上 write_file 不会如此,容错) - } - Some(generate_diff(&old_content, new_content)) - } - Err(_) => None, // 文件不存在(新建)/ 无读权限 → 无 diff,前端显新 content - } -} - /// 处理流式接收的工具调用:Low 风险并行执行(join_all),Med/High 进审批门控 /// 返回待审批的工具数量(0 = 全部自动执行完成) pub(crate) async fn process_tool_calls( diff --git a/src-tauri/src/commands/ai/audit/utils.rs b/src-tauri/src/commands/ai/audit/utils.rs new file mode 100644 index 0000000..8119558 --- /dev/null +++ b/src-tauri/src/commands/ai/audit/utils.rs @@ -0,0 +1,39 @@ +//! audit 共享纯 helper:RiskLevel ↔ 字符串转换 + 字符串安全截断。 +//! +//! 第五批从 audit/mod.rs 抽离,行为零变更: +//! - `risk_str` / `risk_from_str`:finalize / restore 通过 `super::` 引用(pub(crate))。 +//! - `truncate_chars`:仅 mod.rs 的 list_tool_executions DTO 装配用(pub(super))。 +//! +//! re-export 维持原路径透明:`super::risk_str` / `super::risk_from_str` / `super::truncate_chars` +//! 在 audit/mod.rs 内通过 `use` 引入后裸名可用,外部子模块的 `super::risk_*` 引用不变。 + +use df_ai::ai_tools::RiskLevel; + +/// RiskLevel → 审计记录字符串(low/medium/high) +pub(crate) fn risk_str(r: RiskLevel) -> &'static str { + match r { + RiskLevel::Low => "low", + RiskLevel::Medium => "medium", + RiskLevel::High => "high", + } +} + +/// 审计记录字符串 → RiskLevel(启动重建 pending_approvals 用,未知串返回 None 跳过) +pub(crate) fn risk_from_str(s: &str) -> Option { + match s { + "low" => Some(RiskLevel::Low), + "medium" => Some(RiskLevel::Medium), + "high" => Some(RiskLevel::High), + _ => None, + } +} + +/// 截断字符串到 max 字符(按 char_indices 边界切,避免切坏中文/emoji) +pub(super) fn truncate_chars(s: &str, max: usize) -> String { + if s.chars().count() <= max { + return s.to_string(); + } + let mut out: String = s.chars().take(max).collect(); + out.push('…'); + out +}