From d7e28458812b1fbe4daf8604232ee62c9aca3132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sat, 1 Aug 2026 11:43:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E6=96=AD=E8=B7=AF?= =?UTF-8?q?=E5=99=A8=E5=A4=B1=E8=B4=A5=E5=88=A4=E5=AE=9A=E7=BA=AF=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96(=E5=A4=B1=E8=B4=A5=20content=20=E5=8C=85=20J?= =?UTF-8?q?SON=20envelope,is=5Ffailure=5Fcontent=20=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=B8=8D=E8=A7=A3=E6=9E=90=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=96=87=E6=9C=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/ai/agentic/mod.rs | 21 +++++++++++++++------ src-tauri/src/commands/ai/audit/mod.rs | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/commands/ai/agentic/mod.rs b/src-tauri/src/commands/ai/agentic/mod.rs index 25292ae..d3c8ee0 100644 --- a/src-tauri/src/commands/ai/agentic/mod.rs +++ b/src-tauri/src/commands/ai/agentic/mod.rs @@ -1722,13 +1722,22 @@ async fn count_recent_failures( .unwrap_or((0u32, String::new())) } -// ── is_failure_content: 失败判定(禁止/跳过重试/失败/Error 关键词) ── +// ── is_failure_content: 纯结构化判定(只读字段,绝不解析内容文本) ── +// 原理:工具成败是执行层的结构化事实(exit_code / status),断路器只读字段。内容文本(无论含 +// error/失败/任何词)绝不参与判定 —— 这样读含 error 字样的代码、搜"失败"的结果等成功工具内容 +// 永远不会被判失败(根除 conv 09e7abfa 的误熔断)。 +// 失败 content 由 audit/mod.rs 包成 JSON envelope {"status":"failed","error":...}; +// 成功 content 是 result JSON(run_command 含 exit_code,其他工具无 status 字段即成功)或 namespace:// 占位符。 +// 非 JSON(namespace 占位符 / 重试 guard skip 纯文本)→ 无结构化失败信号 → 不判(false)。 fn is_failure_content(content: &str) -> bool { - content.starts_with("禁止") - || content.starts_with("已跳过重试") - || content.contains("失败") - || content.contains("Error") - || content.contains("error") + let Ok(v) = serde_json::from_str::(content) else { + return false; // namespace:// 占位符 / 重试 guard skip 等 → 无结构化失败信号,不判 + }; + if let Some(exit) = v.get("exit_code").and_then(|x| x.as_i64()) { + return exit != 0; // run_command 等:非零退出 = 真失败 + } + matches!(v.get("status").and_then(|s| s.as_str()), Some("failed") | Some("error")) + || v.get("error").is_some() } // ── summarize_tool_results: tool_result view-only 摘要压缩(扁平抽自原嵌套 map 块) ── diff --git a/src-tauri/src/commands/ai/audit/mod.rs b/src-tauri/src/commands/ai/audit/mod.rs index 9c185ff..8e84d80 100644 --- a/src-tauri/src/commands/ai/audit/mod.rs +++ b/src-tauri/src/commands/ai/audit/mod.rs @@ -503,7 +503,12 @@ pub(crate) async fn process_tool_calls( { let mut session = session_arc.lock().await; // T2: 大工具结果进 namespace - let msg_content = if df_ai::namespace_store::should_use_namespace(&content, &draft.name) { + // 失败包结构化 envelope {"status":"failed","error":...}:让断路器按 status 判 + // (不解析内容文本,理论可靠)。emit 给前端的 result 与 audit 仍用原 err_msg 纯文本 + // (前端卡片/审计可读,与断路器信号解耦)。 + let msg_content = if status == "failed" { + serde_json::json!({"status":"failed","error":&content}).to_string() + } else if df_ai::namespace_store::should_use_namespace(&content, &draft.name) { session.namespace_store.store(&draft.name, &content) } else { content.clone() @@ -622,7 +627,12 @@ pub(crate) async fn process_tool_calls( { let mut session = session_arc.lock().await; // T2: 大工具结果进 namespace,主队列只留引用 - let msg_content = if df_ai::namespace_store::should_use_namespace(&content, &draft.name) { + // 失败包结构化 envelope {"status":"failed","error":...}:让断路器按 status 判 + // (不解析内容文本,理论可靠)。emit 给前端的 result 与 audit 仍用原 err_msg 纯文本 + // (前端卡片/审计可读,与断路器信号解耦)。 + let msg_content = if status == "failed" { + serde_json::json!({"status":"failed","error":&content}).to_string() + } else if df_ai::namespace_store::should_use_namespace(&content, &draft.name) { session.namespace_store.store(&draft.name, &content) } else { content.clone()