重构: 后端编排拆解与DRY债收敛及命名纠偏
阶段1拆解: stream 系外移 stream_relay.rs,mod.rs 3048→2607,loop_body 502→83 技能收尾: regenerate/edit 回溯补技能,新增 read_skill 工具,项目级技能扫描 DRY: emit 双写 46 处收敛 event_bus 正典,KV 引号归一收口,LOCK-SLOW 33 处单行化 警示骨架 4→1 与压缩兜底 2→1,噪音目录档位收敛,truncate 同名陷阱消除 工具元数据落 ToolMeta 八维,7 张 match 表改查 registry,parity 测试锁等价 命名: is_noise_entry/teardown_round/probe_missed_terminal_event 等 6 项纠偏
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri::AppHandle;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
@@ -16,6 +16,7 @@ use df_storage::crud::AiToolExecutionRepo;
|
||||
use df_storage::db::Database;
|
||||
|
||||
use super::cache::{cache_hit_warning, find_cached_high_risk_result, pending_placeholder_for};
|
||||
use super::super::event_bus::emit_ai_chat_event;
|
||||
use super::diff::build_write_file_diff;
|
||||
use super::reason::build_approval_reason;
|
||||
use super::record::audit_tool_call;
|
||||
@@ -180,8 +181,7 @@ pub(super) async fn insert_pending_approval(
|
||||
result: serde_json::Value::String(skip_msg.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
audit_tool_call(audit_repo, conv_id, &draft.id, &draft.name, &draft.args, "skipped_retry", risk_level, Some(skip_msg), Some("auto_retry_guard"), current_message_id).await;
|
||||
return;
|
||||
}
|
||||
@@ -214,8 +214,7 @@ pub(super) async fn insert_pending_approval(
|
||||
diff: approval_diff,
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
audit_tool_call(audit_repo, conv_id, &draft.id, &draft.name, &draft.args, "pending", risk_level, None, None, current_message_id).await;
|
||||
}
|
||||
|
||||
@@ -322,8 +321,7 @@ pub(super) async fn handle_approval_tool(
|
||||
dir: dir_label.clone(),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
// 收集后循环外 spawn 执行,对齐 Low risk 不持锁(原注释 L593 声称"锁外"但代码持锁,
|
||||
// run_command 慢命令会阻塞同会话所有触 state.ai_session 的 IPC,CR-51 修此)
|
||||
trust_hits.push((draft, args, dir_label, risk_level));
|
||||
@@ -357,8 +355,7 @@ pub(super) async fn handle_approval_tool(
|
||||
result: serde_json::Value::String(warned_content.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
// 审计:去重命中记一条(status 透传缓存来源 completed/rejected/failed,SW-260618-16;decided_by=auto_dedup),不进 pending
|
||||
audit_tool_call(audit_repo, conv_id, &draft.id, &draft.name, &draft.args, &status, risk_level, Some(warned_content), Some("auto_dedup"), current_message_id).await;
|
||||
return;
|
||||
|
||||
@@ -56,8 +56,12 @@ pub(crate) fn is_pending_placeholder(content: &str) -> bool {
|
||||
/// AC-2 ②:list_directory 目录级去重(cache key 只取 path,同目录换参数也命中)后,
|
||||
/// 通用文案「换参数或换工具」对同目录无效(换 max_depth 也仍命中),故目录类工具
|
||||
/// 用专用文案:明确告知「目录已列过 + 内容未变」,引导换 grep/search_files 定向搜索。
|
||||
///
|
||||
/// 目录类判定查注册元数据(cache_path_key 维度,DRY 债 #5 收敛),与
|
||||
/// readonly_cache_args_key 的目录级 key 同一判据,不再各自手写工具名 match。
|
||||
pub(crate) fn cache_hit_warning(tool_name: &str, hit_count: u32) -> String {
|
||||
if matches!(tool_name, "list_directory" | "list_dir") {
|
||||
crate::commands::ai::tool_registry::ensure_tool_meta_seeded();
|
||||
if df_ai::ai_tools::cache_path_key_only(tool_name) {
|
||||
format!(
|
||||
"[重复调用拦截] 目录 {} 此前已列过(第 {} 次命中缓存),目录内容未变,以下为缓存结果。不要重复列同一目录,如需查找文件请改用 grep/search_files 定向搜索,或换其他目录。\n\n",
|
||||
tool_name, hit_count
|
||||
@@ -212,14 +216,16 @@ fn canonical_args_key(args: &serde_json::Value) -> String {
|
||||
///
|
||||
/// list_directory 同目录换参数(recursive/max_depth/skip_noise_dirs 不同)也判重复——
|
||||
/// 目录内容没变,换深度/递归参数重列是重复探索(实证 09e7abfa:同一目录列 3 次)。
|
||||
/// 故 list_directory 的缓存 key 只取 path(忽略其他参数),同目录即命中缓存,
|
||||
/// 故声明 cache_path_key 的工具(list_directory,DRY 债 #5 收敛后查注册元数据)
|
||||
/// 缓存 key 只取 path(忽略其他参数),同目录即命中缓存,
|
||||
/// 后续命中返回缓存 + 专用警告(cache_hit_warning 目录分支)。
|
||||
/// 其他只读工具仍走全参 JSON 归一(键序无关,见 canonical_args_key)。
|
||||
///
|
||||
/// pub(crate):供 audit/mod.rs 轮内去重复用同一 key 口径(同源同 key,
|
||||
/// 跨轮缓存 + 轮内去重判定一致,防两处 key 漂移致去重误判)。
|
||||
pub(crate) fn readonly_cache_args_key(tool_name: &str, args: &serde_json::Value) -> String {
|
||||
if matches!(tool_name, "list_directory" | "list_dir") {
|
||||
crate::commands::ai::tool_registry::ensure_tool_meta_seeded();
|
||||
if df_ai::ai_tools::cache_path_key_only(tool_name) {
|
||||
args.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
@@ -241,9 +247,9 @@ pub(crate) fn readonly_cache_args_key(tool_name: &str, args: &serde_json::Value)
|
||||
/// 跳过真实执行(也跳过 emit Started/Completed——直接走缓存回填路径)。
|
||||
///
|
||||
/// **安全边界**(对照 find_cached_high_risk_result 严格收紧):
|
||||
/// - 仅匹配白名单只读幂等工具(READONLY_CACHE_TOOLS):read_file/list_directory/
|
||||
/// search_files/grep/read_symbol。写工具(write_file/patch_file/run_command 等)
|
||||
/// 永不缓存,避免误伤。
|
||||
/// - 仅匹配只读幂等工具(readonly 元数据维度,DRY 债 #5 收敛后查注册元数据,
|
||||
/// 现 read_file/read_symbol/list_directory/search_files/grep 声明)。
|
||||
/// 写工具(write_file/patch_file/run_command 等)未声明,永不缓存,避免误伤。
|
||||
/// - 仅匹配 **status=completed** 的成功结果(失败/拒绝不缓存,可能重试成功)。
|
||||
/// - 仅匹配 **非占位** 结果(排除 pending_placeholder 占位)。
|
||||
/// - args 走 JSON 规范化比较(键序无关),与 find_cached_high_risk_result 一致。
|
||||
@@ -260,8 +266,9 @@ pub(crate) async fn find_cached_readonly_result(
|
||||
) -> Option<(String, u32)> {
|
||||
use df_ai::provider::MessageRole;
|
||||
|
||||
// 白名单:只读幂等工具才缓存。写工具/有副作用工具永不缓存。
|
||||
if !READONLY_CACHE_TOOLS.contains(&tool_name) {
|
||||
// 只读幂等工具才缓存。写工具/有副作用工具永不缓存。
|
||||
crate::commands::ai::tool_registry::ensure_tool_meta_seeded();
|
||||
if !df_ai::ai_tools::is_readonly(tool_name) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -332,17 +339,8 @@ pub(crate) async fn find_cached_readonly_result(
|
||||
Some((content, hit_count))
|
||||
}
|
||||
|
||||
/// 可缓存的只读幂等工具白名单。
|
||||
/// 仅含纯读取/查询类工具:同参多次调用结果一致,缓存命中安全。
|
||||
/// 写工具/有副作用工具(write_file/patch_file/run_command/create_*/update_*/delete_* 等)
|
||||
/// 永不进此白名单——它们每次执行都可能改变状态,缓存会掩盖变化。
|
||||
const READONLY_CACHE_TOOLS: &[&str] = &[
|
||||
"read_file", // 读文件内容(同 path+offset+limit+search 参数 → 同结果)
|
||||
"read_symbol", // AST 符号读取(同 path+symbol+full → 同结果)
|
||||
"list_directory", // 列目录(AC-2 ②:缓存 key 只取 path,同目录换 recursive/max_depth 也判重复)
|
||||
"search_files", // 搜文件名(同 path+pattern+offset+limit → 同结果)
|
||||
"grep", // 搜文件内容(同 path+pattern+mode → 同结果)
|
||||
];
|
||||
/// 只读缓存白名单已收敛为注册元数据(readonly 维度,DRY 债 #5):
|
||||
/// find_cached_readonly_result 改查 df_ai::ai_tools::is_readonly,本文件不再持名单。
|
||||
|
||||
/// 递归对 JSON 对象的键做字典序排序(就地),数组成员也递归排序。
|
||||
fn sort_object_keys(v: &mut serde_json::Value) {
|
||||
@@ -421,15 +419,18 @@ fn contains_listing_command(command: &str) -> bool {
|
||||
|| contains_word(&lower, "ls")
|
||||
}
|
||||
|
||||
/// 扫描会话历史,取所有 list_directory 已列过的目录路径(去重,保留原始大小写)。
|
||||
/// 扫描会话历史,取所有目录级缓存工具(cache_path_key 声明者,现即 list_directory)
|
||||
/// 已列过的目录路径(去重,保留原始大小写)。
|
||||
///
|
||||
/// 与 find_cached_readonly_result 同源读 assistant tool_calls;仅取 list_directory
|
||||
/// 的 args.path,忽略递归/深度参数(② 同语义:同目录即算已列过)。
|
||||
/// 与 find_cached_readonly_result 同源读 assistant tool_calls;仅取 args.path,
|
||||
/// 忽略递归/深度参数(② 同语义:同目录即算已列过)。工具判定查注册元数据
|
||||
/// (cache_path_key 维度,DRY 债 #5 收敛),与目录级缓存 key 同一判据。
|
||||
async fn listed_directory_paths(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
) -> Vec<String> {
|
||||
use df_ai::provider::MessageRole;
|
||||
crate::commands::ai::tool_registry::ensure_tool_meta_seeded();
|
||||
let session = session_arc.lock().await;
|
||||
let Some(conv) = session.conv_read(conv_id) else { return Vec::new() };
|
||||
let mut dirs: Vec<String> = Vec::new();
|
||||
@@ -440,7 +441,7 @@ async fn listed_directory_paths(
|
||||
}
|
||||
let Some(tcs) = m.tool_calls.as_ref() else { continue };
|
||||
for tc in tcs {
|
||||
if tc.function.name != "list_directory" {
|
||||
if !df_ai::ai_tools::cache_path_key_only(&tc.function.name) {
|
||||
continue;
|
||||
}
|
||||
let Ok(args) = serde_json::from_str::<serde_json::Value>(&tc.function.arguments) else {
|
||||
@@ -494,9 +495,10 @@ pub(crate) async fn detect_listing_bypass(
|
||||
None
|
||||
}
|
||||
|
||||
/// AC-2 ③:目录列举绕行警告注入(system 消息首位,对齐 G2/AC-2① 软提示模式)。
|
||||
/// AC-2 ③:目录列举绕行警告注入(system 消息首位)。
|
||||
///
|
||||
/// 只警告不硬拒:命令照常执行/审批,误报最多多一条提示,不阻断工作流。
|
||||
/// 插入骨架经 agentic::helpers::insert_system_notice 复用(DRY #7,与 G2/AC-2① 同一实现)。
|
||||
pub(crate) async fn insert_listing_bypass_warning(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
@@ -506,16 +508,13 @@ pub(crate) async fn insert_listing_bypass_warning(
|
||||
"⚠ 检测到用 run_command 列目录({}) 绕行 list_directory 去重。列目录请用 list_directory 工具,如需查找文件请用 grep/search_files 定向搜索。",
|
||||
dir
|
||||
);
|
||||
let mut session = session_arc.lock().await;
|
||||
if session.per_conv.contains_key(conv_id) {
|
||||
let conv = session.conv(conv_id);
|
||||
conv.messages.insert_at(0, ChatMessage::system(&warn_text));
|
||||
tracing::info!(
|
||||
conv_id = %conv_id,
|
||||
dir = %dir,
|
||||
"[ai] AC-2 目录列举绕行:注入警告(只警告不硬拒)"
|
||||
);
|
||||
}
|
||||
crate::commands::ai::agentic::helpers::insert_system_notice(
|
||||
session_arc,
|
||||
conv_id,
|
||||
&warn_text,
|
||||
&format!("AC-2 目录列举绕行:注入警告(只警告不硬拒) dir={}", dir),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -11,33 +11,13 @@ use tauri::{AppHandle, Emitter};
|
||||
/// AR-11(方案A):工具名 → (entity, action) 映射,用于数据变更联动刷新。
|
||||
///
|
||||
/// 仅数据变更类工具返回 Some,其余工具返回 None(不 emit df-data-changed)。
|
||||
/// 映射规则:
|
||||
/// - create_project/create_idea/create_task → (project/idea/task, create)
|
||||
/// - update_project/update_task/update_idea → (..., update)
|
||||
/// - delete_project/delete_task/delete_idea/purge_project → (..., delete)
|
||||
/// - restore_project → (project, update)(恢复等同状态变更,前端走 update 刷新)
|
||||
/// - bind_directory → (project, update)(绑定目录属项目字段更新)
|
||||
/// 映射查注册元数据(entity_action 维度,DRY 债 #5 收敛),在 declare_tool! 注册点声明:
|
||||
/// create_*(project/idea/task, create)、update_*(..., update)、delete_*/purge_project
|
||||
/// (..., delete)、restore_project/bind_directory → (project, update)、advance_task →
|
||||
/// (task, update)(状态推进对齐 update_task)、write_file/patch_file → (file, update)。
|
||||
pub(super) fn data_change_for_tool(name: &str) -> Option<(&'static str, &'static str)> {
|
||||
let (entity, action) = match name {
|
||||
"create_project" => ("project", "create"),
|
||||
"create_task" => ("task", "create"),
|
||||
"create_idea" => ("idea", "create"),
|
||||
"update_project" => ("project", "update"),
|
||||
"update_task" => ("task", "update"),
|
||||
"update_idea" => ("idea", "update"),
|
||||
// advance_task 改任务状态 = 任务数据变更,MCP 走 HTTP 形态时让 GUI 前端
|
||||
// listen df-data-changed 刷新任务列表(此前不在映射 → 状态推进后 GUI 不刷新)。
|
||||
// 数据变更机制是 entity 级(无 id),映射到 (task, update) 对齐 update_task。
|
||||
"advance_task" => ("task", "update"),
|
||||
"delete_project" | "purge_project" => ("project", "delete"),
|
||||
"delete_task" => ("task", "delete"),
|
||||
"delete_idea" => ("idea", "delete"),
|
||||
"restore_project" => ("project", "update"),
|
||||
"bind_directory" => ("project", "update"),
|
||||
"write_file" | "patch_file" => ("file", "update"),
|
||||
_ => return None,
|
||||
};
|
||||
Some((entity, action))
|
||||
crate::commands::ai::tool_registry::ensure_tool_meta_seeded();
|
||||
df_ai::ai_tools::entity_action(name)
|
||||
}
|
||||
|
||||
/// AR-11(方案A):工具执行成功后按映射 emit "df-data-changed" { entity, action }。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
@@ -11,6 +11,8 @@ use df_ai::provider::ChatMessage;
|
||||
use df_storage::crud::AiToolExecutionRepo;
|
||||
use df_storage::db::Database;
|
||||
|
||||
use super::event_bus::emit_ai_chat_event;
|
||||
use crate::state::{SettingsNorm, log_slow_lock};
|
||||
use super::{AiChatEvent, AiSession, ApprovalKind, PathAuthRequest, PendingApproval, ToolCallDraft};
|
||||
use super::tools::entity_resolve;
|
||||
|
||||
@@ -142,8 +144,7 @@ async fn execute_with_heartbeat(
|
||||
break;
|
||||
}
|
||||
let ev = AiChatEvent::AiHeartbeat { conversation_id: Some(conv.clone()) };
|
||||
let _ = app_h.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_h.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_h, ev);
|
||||
}
|
||||
});
|
||||
// RAII:execute 无论 Ok/Err/panic(unwind),_guard drop 自动 stop+abort,无心跳 task 泄漏。
|
||||
@@ -233,10 +234,7 @@ pub(crate) async fn process_tool_calls(
|
||||
let session = session_arc.lock().await;
|
||||
let __id = session.conv_read(conv_id)
|
||||
.and_then(|c| c.messages.last_assistant_message_id());
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:192 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
log_slow_lock(__lock_t, "process_tool_calls:192");
|
||||
__id
|
||||
},
|
||||
).await {
|
||||
@@ -260,8 +258,7 @@ pub(crate) async fn process_tool_calls(
|
||||
args: args.clone(),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
(idx, draft, args)
|
||||
})
|
||||
.collect();
|
||||
@@ -283,18 +280,14 @@ pub(crate) async fn process_tool_calls(
|
||||
let __lock_t = std::time::Instant::now();
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &err_payload));
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:268 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
log_slow_lock(__lock_t, "process_tool_calls:268");
|
||||
}
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
id: draft.id.clone(),
|
||||
result: serde_json::Value::String(err_payload.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
let risk = tools_arc.get(&draft.name).map(|t| t.risk_level).unwrap_or(RiskLevel::High);
|
||||
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args,
|
||||
"failed", risk, Some(err_payload.clone()), Some("auto_resolve_fail"), current_message_id).await;
|
||||
@@ -306,16 +299,12 @@ pub(crate) async fn process_tool_calls(
|
||||
// 自动执行范围三档读取(前置到路径授权预校验之前:两处门控共用)。
|
||||
// KV df-ai-auto-execute-mode 取值 low/medium/all,默认 low;
|
||||
// low 仅 Low 自动(等价旧行为),medium Low+Medium,all 全自动无审批(完全 AI 接管)。
|
||||
// get_string_norm 归一去引号(前端 JSON.stringify 落库带引号),不归一则 medium/all 档静默失效全走审批。
|
||||
let auto_exec_mode = app_handle
|
||||
.state::<crate::state::AppState>()
|
||||
.settings
|
||||
.get("df-ai-auto-execute-mode")
|
||||
.get_string_norm("df-ai-auto-execute-mode")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
// 前端 appSettings.set 以 JSON.stringify 落库(字符串值带引号,"all" != all),
|
||||
// 不归一则 medium/all 档静默失效全走审批(同 prompt.rs custom_prompt 的 trim 先例)。
|
||||
.map(|v| v.trim_matches('"').to_string())
|
||||
.unwrap_or_else(|| "low".to_string());
|
||||
|
||||
// ── 文件工具路径授权预校验 ──
|
||||
@@ -334,16 +323,12 @@ pub(crate) async fn process_tool_calls(
|
||||
// 「信任所有目录」总开关(KV df-ai-path-auth-bypass,默认关):开启后白名单外路径不再挂起
|
||||
// 授权弹窗(NeedsAuth 时直接放行),黑名单拦截也一并放行(产品拍板:体验优先,含敏感目录)。
|
||||
// 读取失败按关处理(fail-safe,默认行为不变);前端设置页改后下一轮 process_tool_calls 即生效。
|
||||
// get_bool_norm 归一比对(前端 JSON.stringify 落库带引号),默认 false 由调用方显式传参。
|
||||
let path_auth_bypass = app_handle
|
||||
.state::<crate::state::AppState>()
|
||||
.settings
|
||||
.get(PATH_AUTH_BYPASS_KEY)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
// 前端 appSettings.set 以 JSON.stringify 落库(带引号),归一后比对(同 df-ai-trace-approve 先例)
|
||||
.map(|v| { let v = v.trim_matches('"'); v == "true" || v == "1" })
|
||||
.unwrap_or(false);
|
||||
.get_bool_norm(PATH_AUTH_BYPASS_KEY, false)
|
||||
.await;
|
||||
// 同步进程级黑名单放行标志:validate_path(tool_registry)无 bypass 参数,靠此标志放行黑名单;
|
||||
// 每轮工具执行前刷新,与下方 check_file_tool_auth 的入参 bypass 同源同值,两处拦截点语义一致。
|
||||
path_auth::set_blacklist_bypass(path_auth_bypass);
|
||||
@@ -385,10 +370,7 @@ pub(crate) async fn process_tool_calls(
|
||||
let __lock_t = std::time::Instant::now();
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &err_msg));
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:262 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
log_slow_lock(__lock_t, "process_tool_calls:262");
|
||||
}
|
||||
// L3 emit 双写:路径黑名单拒绝 emit Completed 双路发布。
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
@@ -396,8 +378,7 @@ pub(crate) async fn process_tool_calls(
|
||||
result: serde_json::Value::String(err_msg.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
// 审计:路径黑名单拒绝(decided_by=auto_blacklist),留痕可追溯
|
||||
let risk_level = tools_arc.get(&draft.name).map(|t| t.risk_level).unwrap_or(RiskLevel::High);
|
||||
audit_tool_call(
|
||||
@@ -433,18 +414,14 @@ pub(crate) async fn process_tool_calls(
|
||||
let __lock_t = std::time::Instant::now();
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &skip_msg));
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:305 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
log_slow_lock(__lock_t, "process_tool_calls:305");
|
||||
}
|
||||
let ev = AiChatEvent::AiToolCallCompleted {
|
||||
id: draft.id.clone(),
|
||||
result: serde_json::Value::String(skip_msg.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
let risk_level = tools_arc.get(&draft.name).map(|t| t.risk_level).unwrap_or(RiskLevel::High);
|
||||
audit_tool_call(
|
||||
&audit_repo, conv_id, &draft.id, &draft.name, &draft.args,
|
||||
@@ -473,10 +450,7 @@ pub(crate) async fn process_tool_calls(
|
||||
);
|
||||
// 占位 tool_result(与 RiskLevel 审批一致),ai_authorize_dir 批准后替换为真实结果。
|
||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &pending_placeholder_for(&draft.id)));
|
||||
let __hold = __lock_t.elapsed();
|
||||
if __hold > std::time::Duration::from_millis(30) {
|
||||
eprintln!("[LOCK-SLOW] process_tool_calls:325 持锁 {:?} (含 lock 等待)", __hold);
|
||||
}
|
||||
log_slow_lock(__lock_t, "process_tool_calls:325");
|
||||
}
|
||||
tracing::debug!(target: "ai_dirauth", conv = %conv_id, tool = %draft.name, tc_id = %draft.id, "emit AiDirAuthRequired(路径授权挂起,等 ai_authorize_dir 恢复)");
|
||||
let ev = AiChatEvent::AiDirAuthRequired {
|
||||
@@ -486,8 +460,7 @@ pub(crate) async fn process_tool_calls(
|
||||
dir: dir_str,
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
let risk_level = tools_arc.get(&draft.name).map(|t| t.risk_level).unwrap_or(RiskLevel::High);
|
||||
audit_tool_call(
|
||||
&audit_repo, conv_id, &draft.id, &draft.name, &draft.args,
|
||||
@@ -507,16 +480,12 @@ pub(crate) async fn process_tool_calls(
|
||||
// 也进此向量,需透传真实等级,审计方可追溯「all 模式执行了多少高危命令」。
|
||||
let mut low_risk: Vec<(ToolCallDraft, serde_json::Value, RiskLevel)> = Vec::new();
|
||||
// F-#97 诊断:用户报「选 all 仍审批」时辅助定位 KV 是否真落库。
|
||||
// df-ai-trace-approve 开关控制(默认开)。常见误以为已生效但实际未点确认按钮致 KV 未落。
|
||||
// df-ai-trace-approve 开关控制(默认开,get_bool_norm 默认值调用方传参)。常见误以为已生效但实际未点确认按钮致 KV 未落。
|
||||
let trace_on = app_handle
|
||||
.state::<crate::state::AppState>()
|
||||
.settings
|
||||
.get("df-ai-trace-approve")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| { let v = v.trim_matches('"'); v == "true" || v == "1" })
|
||||
.unwrap_or(true);
|
||||
.get_bool_norm("df-ai-trace-approve", true)
|
||||
.await;
|
||||
if trace_on {
|
||||
tracing::info!(
|
||||
conv_id = %conv_id,
|
||||
@@ -570,8 +539,7 @@ pub(crate) async fn process_tool_calls(
|
||||
result: serde_json::Value::String(content.clone()),
|
||||
conversation_id: Some(conv_clone),
|
||||
};
|
||||
let _ = app_clone.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_clone.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_clone, ev);
|
||||
// 数据变更类工具执行成功后 emit df-data-changed 联动刷新
|
||||
// (write_file/run_command 不在 data_change_for_tool 映射内,emit_data_changed 内部 None 即 noop)
|
||||
emit_data_changed(&app_clone, &draft.name);
|
||||
@@ -585,8 +553,7 @@ pub(crate) async fn process_tool_calls(
|
||||
result: serde_json::Value::String(content.clone()),
|
||||
conversation_id: Some(conv_clone),
|
||||
};
|
||||
let _ = app_clone.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_clone.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_clone, ev);
|
||||
(draft, risk_level, Err(content))
|
||||
}
|
||||
}
|
||||
@@ -668,8 +635,7 @@ pub(crate) async fn process_tool_calls(
|
||||
result: serde_json::Value::String(warned_content.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
// 短 lock 段:push tool_result(纯写,无 await)
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
@@ -709,8 +675,7 @@ pub(crate) async fn process_tool_calls(
|
||||
result: val.clone(),
|
||||
conversation_id: Some(conv_clone),
|
||||
};
|
||||
let _ = app_clone.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_clone.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_clone, ev);
|
||||
// AR-11(方案A):数据变更工具执行成功后 emit df-data-changed,
|
||||
// 前端 store listen 刷新列表(仅命中映射的工具 emit,见 emit_data_changed)。
|
||||
emit_data_changed(&app_clone, &draft.name);
|
||||
@@ -729,8 +694,7 @@ pub(crate) async fn process_tool_calls(
|
||||
result: serde_json::Value::String(err_msg.clone()),
|
||||
conversation_id: Some(conv_clone),
|
||||
};
|
||||
let _ = app_clone.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_clone.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_clone, ev);
|
||||
(draft, serde_json::Value::Null, risk_level, Err(err_msg))
|
||||
}
|
||||
}
|
||||
@@ -831,8 +795,7 @@ pub(crate) async fn process_tool_calls(
|
||||
result: serde_json::Value::String(warned_content.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||
let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
// 短 lock 段:push tool_result(纯写,无 await)
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
|
||||
@@ -40,28 +40,13 @@ pub(crate) static BLACKLIST_BYPASS_TEST_LOCK: std::sync::Mutex<()> = std::sync::
|
||||
/// 提取文件工具的路径参数(用于路径授权预校验)。
|
||||
///
|
||||
/// 仅对走 resolve_workspace_path 校验的文件工具返回路径;非文件工具返回空 Vec(不预校验)。
|
||||
/// - 单路径工具(read_file/write_file/list_directory/patch_file/file_info/append_file/delete_file/search_files/grep)
|
||||
/// 取 args["path"]
|
||||
/// - rename_file 双路径取 args["from"] + args["to"](两个都需授权)
|
||||
/// - run_command 不返回(它只走 validate_path 黑名单,不限定 workspace,High risk 靠人工审批)
|
||||
/// 路径参数键查注册元数据(path_args 维度,DRY 债 #5 收敛):单路径工具(read_file/
|
||||
/// write_file/list_directory/patch_file/file_info/append_file/delete_file/search_files/grep)
|
||||
/// 声明 ["path"],rename_file 声明 ["from","to"](两个都需授权),其余工具默认空(不预校验)。
|
||||
/// run_command 不声明(只走 validate_path 黑名单,不限定 workspace,High risk 靠人工审批)。
|
||||
pub(crate) fn extract_file_tool_paths(tool_name: &str, args: &serde_json::Value) -> Vec<String> {
|
||||
match tool_name {
|
||||
"rename_file" => {
|
||||
let mut v = Vec::new();
|
||||
if let Some(from) = args.get("from").and_then(|x| x.as_str()) {
|
||||
v.push(from.to_string());
|
||||
}
|
||||
if let Some(to) = args.get("to").and_then(|x| x.as_str()) {
|
||||
v.push(to.to_string());
|
||||
}
|
||||
v
|
||||
}
|
||||
"read_file" | "write_file" | "list_directory" | "patch_file" | "file_info"
|
||||
| "append_file" | "delete_file" | "search_files" | "grep" => {
|
||||
args.get("path").and_then(|x| x.as_str()).map(|s| vec![s.to_string()]).unwrap_or_default()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
crate::commands::ai::tool_registry::ensure_tool_meta_seeded();
|
||||
df_ai::ai_tools::extract_paths(df_ai::ai_tools::path_arg_keys(tool_name), args)
|
||||
}
|
||||
|
||||
/// 路径授权预校验结果。
|
||||
@@ -124,10 +109,11 @@ pub(crate) fn check_file_tool_auth(
|
||||
}
|
||||
if pending_dirs.is_empty() {
|
||||
FileToolAuthOutcome::Authorized
|
||||
} else if tool_name == "search_files" {
|
||||
} else if df_ai::ai_tools::denies_unauthorized(tool_name) {
|
||||
// 核心设计5:search_files 兜底 — 盲搜语义应拒不应询。
|
||||
// search_files 仅在已授权目录可用;用户已通过 @项目 提供项目上下文,无需搜索文件系统。
|
||||
// (黑名单已在上方 for 循环内短路返 Denied,走到此处均为 NeedsAuthorization 场景。)
|
||||
// 拒绝名单查注册元数据(deny_unauthorized 维度,DRY 债 #5 收敛,现仅 search_files 声明);
|
||||
// 其他文件工具保持原 NeedsAuth 弹窗逻辑完全不变。
|
||||
FileToolAuthOutcome::Denied(
|
||||
"search_files 仅在已授权目录可用;请用 @[项目] 引用或提供绝对路径,不要盲搜文件系统。".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user