修复: 全库走查 P0+P1(df-mcp 数据丢失/evaluate 拆 + df-execute probe_pwsh 死缓存/超时/单源 + ai_self_review 注入隔离)

- df-mcp P0: update_idea/project/task 缺省回退 existing(治部分更新清空 title 丢数据)+ P1: evaluate_idea 拆只读(Low)+score_idea(Medium 写),read-only 不再改库
- df-execute P0: probe_pwsh 死缓存(两 OnceLock 合并 PWSH_CACHE 单源,治 Windows 永走 PS5)+ P1: probe_pwsh 3s 超时防挂起 + detect_shell 复用 shell.rs 单源(探测与执行对齐)
- df-nodes P1: ai_self_review prompt 注入隔离(truncate + XML 标签 <task_output> 数据/指令隔离 + system 声明)
- generate_image: SSRF 集成测试 + b64 OOM 估算纯函数测试(走查测试增强)
This commit is contained in:
lxy
2026-08-02 03:11:44 +08:00
parent dffc4e4851
commit fc249adf17
7 changed files with 1000 additions and 82 deletions
+38 -19
View File
@@ -221,18 +221,27 @@ fn read_windows_version() -> Option<String> {
None
}
/// 探测默认 shell(复用 shell.rs 的 pwsh 探测语义)。
/// 探测默认 shell(Windows 走 shell.rs 单源,Unix 读 SHELL 环境变量)。
///
/// 单源语义(2026-08 修复):Windows 分支不再独立 probe pwsh/powershell,而是:
/// 1) 先调 `shell::probe_pwsh_blocking()` 填充模块级 `PWSH_CACHE`(与 `shell::probe_pwsh()` 异步路径
/// 共用同一 OnceLock + 同一阻塞探测实现,杜绝两套逻辑漂移);
/// 2) 再调 `shell::current_shell()`(同步读 `PWSH_CACHE`),映射 ShellType → prompt 字符串。
/// 后续 `execute()` → `probe_pwsh().await` 直接命中缓存,跳过重复探测。
/// Unix 分支保留原 SHELL 环境变量读取(与执行侧 ShellType::Sh 默认一致,无漂移风险)。
fn detect_shell() -> String {
#[cfg(windows)]
{
// 优先 pwsh(PS7,支持 &&),其次 powershell(PS5),兜底 cmd。
if probe_command_success("pwsh", &["-NoProfile", "-Command", "exit 0"]) {
return "pwsh".to_string();
// 单源填充 PWSH_CACHE(本函数运行在 EnvSnapshot::detect 的 spawn_blocking 内,
// 通常先于首次 execute(),故常是缓存的首次写入者)。
crate::shell::probe_pwsh_blocking();
// 单源读取并映射为 prompt 字符串。
match crate::shell::current_shell() {
crate::shell::ShellType::Pwsh => "pwsh".to_string(),
crate::shell::ShellType::PowerShell => "powershell".to_string(),
crate::shell::ShellType::Cmd => "cmd".to_string(),
crate::shell::ShellType::Sh => "sh".to_string(),
}
if probe_command_success("powershell", &["-NoProfile", "-Command", "exit 0"]) {
return "powershell".to_string();
}
return "cmd".to_string();
}
#[cfg(not(windows))]
{
@@ -301,17 +310,6 @@ fn extract_codepage(text: &str) -> Option<String> {
}
}
/// 执行 `tool args`,成功(true)即工具可用。Windows 加 CREATE_NO_WINDOW 防黑窗。
#[allow(dead_code)] // 仅 Windows 路径调用,非 Windows 静态裁掉
fn probe_command_success(tool: &str, args: &[&str]) -> bool {
let mut cmd = std::process::Command::new(tool);
cmd.args(args);
cmd.stdout(Stdio::null()).stderr(Stdio::null());
#[cfg(windows)]
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
cmd.status().map(|s| s.success()).unwrap_or(false)
}
/// 执行 `tool --version`,解析首行返回版本串。失败/超时返回 None,不阻塞调用方。
///
/// 例:python --version 输出 "Python 3.11.5" → 返回 "3.11.5";git --version 输出
@@ -429,6 +427,27 @@ mod tests {
assert_eq!(a, b, "detect() 应返回同一静态引用");
}
/// 单源不变量回归:prompt 期 shell(env_snapshot.shell)与执行期 shell(current_shell)必须一致。
///
/// 历史 bug:detect_shell 持独立 probe_command_success 探测,与 shell.rs::probe_pwsh 各填各的
/// OnceLock → prompt 告诉 LLM 用 pwsh,执行却走 powershell(或反之)。根本修:二者共用
/// shell.rs 的 probe_pwsh_blocking + PWSH_CACHE 单源。本测试锁定「无漂移」不变量。
#[cfg(windows)]
#[tokio::test]
async fn detect_shell_matches_shell_rs_current_shell() {
// detect() 内 detect_shell → probe_pwsh_blocking 填 PWSH_CACHE,再读 current_shell 映射。
let snap = EnvSnapshot::detect().await;
let current = crate::shell::current_shell();
// 二者必须一致(同一 PWSH_CACHE 单源读出)。
match snap.shell.as_str() {
"pwsh" => assert_eq!(current, crate::shell::ShellType::Pwsh),
"powershell" => assert_eq!(current, crate::shell::ShellType::PowerShell),
"cmd" => assert_eq!(current, crate::shell::ShellType::Cmd),
"sh" => assert_eq!(current, crate::shell::ShellType::Sh),
other => panic!("未知 shell 字符串: {}", other),
}
}
#[test]
fn to_prompt_contains_os_and_shell() {
let snap = EnvSnapshot {
+134 -29
View File
@@ -39,51 +39,156 @@ impl Default for ShellType {
// 优先 pwsh(PS7,支持 && 运算符)——LLM 训练数据 Unix 多,普遍生成 `cd x && y`,
// PS5 不支持 && 致命令失败(实测会话 6acb7f9b `cd ... && git init` InvalidEndOfLine)。
// 探测失败(未装 pwsh)回退 PS5。探测结果 OnceLock 缓存(只探一次)。
// 注:Default trait 为同步签名,这里只能读取已探测的缓存结果(若未探测则返回 false,退回 PowerShell)。
// 真实探测在异步入口 `execute()` 中调用 `probe_pwsh().await`
//
// 【序约束 / 死缓存修复】Default 为同步签名,读模块级单源 PWSH_CACHE(由 probe_pwsh 写入)
// 缓存有两个填充点(均走 probe_pwsh_blocking 单真相源,无漂移):
// 1) env_snapshot::detect_shell → EnvSnapshot::detect()(启动期 spawn_blocking 内,先于 execute);
// 2) execute()/execute_streaming() 异步入口 → probe_pwsh().await(spawn_blocking + 3s 超时)。
// 通常 detect_shell 先跑(EnvSnapshot::detect 在 run_agentic_loop 早期被 await),首次 execute 时
// probe_pwsh 直接命中缓存。若 detect 尚未填充,probe_pwsh 自带探测兜底。任一时刻 build_command →
// ShellType::default() 读到缓存值;未探测(缓存空)返回 PowerShell(PS5)。绝不在 Default 同步路径内 spawn 探测。
//
// 历史 bug(2026-08 修复):原 probe_pwsh_cached() 与 probe_pwsh() 各持一个独立 static OnceLock,
// Default 读的那个永不被填充 → Pwsh 全死代码,Windows 永走 PS5(&& 必失败)。根因:两个 OnceLock
// 非单源 + env_snapshot 另持 probe_command_success 独立探测。根本修:合并为模块级 PWSH_CACHE 单源,
// 写读同源;detect_shell 复用 probe_pwsh_blocking(单一探测实现)。
if cfg!(windows) {
if probe_pwsh_cached() { ShellType::Pwsh } else { ShellType::PowerShell }
if PWSH_CACHE.get().copied().unwrap_or(false) { ShellType::Pwsh } else { ShellType::PowerShell }
} else {
ShellType::Sh
}
}
}
/// 读取 probe_pwsh 的缓存值(未探测返回 false)。供同步路径 `Default` 使用
fn probe_pwsh_cached() -> bool {
static CACHE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
CACHE.get().copied().unwrap_or(false)
/// pwsh(PowerShell 7)可用性的全局单源缓存
///
/// 由异步 `probe_pwsh()`(execute 路径)或同步 `probe_pwsh_blocking()`(detect_shell 路径)填充,
/// 同步路径 `ShellType::default()` / `current_shell()` 读取——写读同源,
/// 杜绝历史上「两个 OnceLock 各填各的、Default 读的永空」死缓存 bug。
///
/// 语义:`get() == None` 表示尚未探测(首次启动 / Windows 未装 pwsh 也仅表示探测未跑或返 false);
/// `get() == Some(true)` 表示已探测且 pwsh 可用。
static PWSH_CACHE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
/// 同步探测 pwsh(PowerShell 7)是否可用(std::Command::status),成功返回 true。
///
/// 这是阻塞 IO 的「单真相源」实现——`probe_pwsh()`(异步,spawn_blocking + 超时)与
/// env_snapshot::detect_shell(windows 同步路径)都调用本函数,二者探测逻辑永远一致,
/// 杜绝「提示告诉 LLM 用 pwsh,执行却走 powershell」的两套逻辑漂移。
///
/// 实现等价于原 `probe_pwsh()` 内的闭包:`pwsh -NoProfile -Command exit 0` 成功即 true;
/// Windows 加 CREATE_NO_WINDOW(0x0800_0000)防黑窗闪现。
///
/// 注:此处无超时——`probe_pwsh()` 在调用方包 `tokio::time::timeout`,
/// `detect_shell()` 则由外层 `EnvSnapshot::detect()` 的 spawn_blocking 5s 超时兜底。
/// 故本函数本身只负责「同步 spawn + status」,超时治理在调用点。
pub(crate) fn probe_pwsh_blocking() -> bool {
let mut cmd = std::process::Command::new("pwsh");
cmd.arg("-NoProfile").arg("-Command").arg("exit 0");
cmd.stdout(Stdio::null()).stderr(Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
cmd.status().map(|s| s.success()).unwrap_or(false)
}
/// 探测 pwsh(PowerShell 7)是否可用(OnceLock 缓存,只探一次)。
/// 探测 pwsh(PowerShell 7)是否可用(OnceLock 缓存,3s 超时,只探一次)。
///
/// LLM 普遍生成 `&&`(Unix 习惯),仅 PS7+ 支持,Windows 自带 PS5 不支持。
/// 探测:成功 spawn `pwsh -Command exit 0` 即可用。同步阻塞仅一次(spawn 极快),
/// Windows 加 CREATE_NO_WINDOW 防黑窗闪现。
/// 探测:成功 spawn `pwsh -Command exit 0` 即可用。Windows 加 CREATE_NO_WINDOW 防黑窗闪现。
///
/// CR-XX:异步化 —— 在异步上下文中通过 `tokio::task::spawn_blocking` 执行阻塞探测,
/// 避免阻塞 tokio runtime。结果仍由 OnceLock 全局共享,只探测一次
/// 异步化 + 超时治理:在异步上下文中通过 `tokio::task::spawn_blocking` 执行阻塞探测,
/// 外包 `tokio::time::timeout(3s)`。结果写入模块级单源 `PWSH_CACHE`,同步 `Default` 路径共享
///
/// 超时/panic 时**不写入 PWSH_CACHE**:
/// - 超时根因往往是 Windows Store Alias / 杀软 hook 拦截 `pwsh` 命令(status() 永不返回);
/// 若错误地把 false 缓存,后续 execute() 会一直走 PS5(&& 必失败),把瞬时环境问题冻结成
/// 「永不可用」错误判定。不 set → 每次 execute 重试,环境恢复后自动回正。
/// - panic(join err)同理,可能是临时线程池异常,不应冻结判定。
/// - 仅「正常完成(Ok(Ok(_)))」时 set 缓存(此时结果是可信的探测产物)。
/// 对齐 env_snapshot.rs:64-80 的「spawn_blocking + timeout + 超时不 set」模式。
async fn probe_pwsh() -> bool {
static CACHE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
if let Some(cached) = CACHE.get() {
if let Some(cached) = PWSH_CACHE.get() {
return *cached;
}
let result = tokio::task::spawn_blocking(|| {
let mut cmd = std::process::Command::new("pwsh");
cmd.arg("-NoProfile").arg("-Command").arg("exit 0");
cmd.stdout(Stdio::null()).stderr(Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
match tokio::time::timeout(
std::time::Duration::from_secs(3),
tokio::task::spawn_blocking(probe_pwsh_blocking),
).await {
Ok(Ok(result)) => {
// 正常完成:best-effort set(多任务竞态以先到者为准,均等价)
let _ = PWSH_CACHE.set(result);
result
}
cmd.status().map(|s| s.success()).unwrap_or(false)
})
.await
.unwrap_or(false);
// 多任务竞态时以先到者为准,均等价
let _ = CACHE.set(result);
result
Ok(Err(join_err)) => {
eprintln!(
"[shell] probe_pwsh spawn_blocking 异常(不缓存,下次 execute 重试): {}",
join_err
);
false
}
Err(_elapsed) => {
eprintln!(
"[shell] probe_pwsh 3s 超时(疑似 Windows Store Alias / 杀软 hook 拦截 pwsh, \
不缓存以免冻结错误判定,下次 execute 重试)"
);
false
}
}
}
/// 当前 shell 类型(同步读取模块级 PWSH_CACHE 单源)。
///
/// 探测在 `probe_pwsh()`(异步,带 3s 超时)或 `probe_pwsh_blocking()`(detect_shell 同步路径)中执行并填充 PWSH_CACHE;此处仅读。
/// 缓存空(未探测 / 探测超时未 set)时返回 `ShellType::default()`(Windows → PowerShell,Unix → Sh)。
/// 单源语义:env_snapshot::detect_shell 与 build_command 共用此判定,杜绝「探测与执行两套逻辑漂移」。
pub fn current_shell() -> ShellType {
ShellType::default()
}
#[cfg(test)]
mod tests {
use super::*;
/// 死缓存回归测试:probe_pwsh 写入后,Default 同步路径必须读到同一缓存值。
///
/// 历史 bug:probe_pwsh_cached() 与 probe_pwsh() 各持独立 OnceLock,Default 读的永空。
/// 本测试通过手动 set 模块级 PWSH_CACHE 后断言 default() 返回 Pwsh,锁定「写读同源」不变量。
#[cfg(windows)]
#[tokio::test]
async fn probe_pwsh_cache_shared_with_default() {
// 探测一次填充缓存(无论机器是否装 pwsh,只要写读同源即应一致)。
// 注:正常完成路径才会 set 缓存;本机 pwsh 探测不超时/不 panic,故 cached 应与 probed 一致。
let probed = probe_pwsh().await;
let cached = PWSH_CACHE.get().copied();
// 探测完成后缓存必已填充(同步路径由此读到)
assert_eq!(cached, Some(probed));
// Default 必须读到与 probe 一致的判定:Pwsh ↔ true,PowerShell ↔ false
let default_shell = ShellType::default();
match probed {
true => assert_eq!(default_shell, ShellType::Pwsh),
false => assert_eq!(default_shell, ShellType::PowerShell),
}
}
/// 不挂死不变量回归:probe_pwsh 必须在有限时间内返回(自带 3s 超时 + spawn_blocking)。
///
/// 历史 bug:probe_pwsh 仅 spawn_blocking 无超时,Windows Store Alias / 杀软 hook 拦截 pwsh 时
/// status() 永不返回 → spawn_blocking 线程永不返回 → probe_pwsh().await 永久挂 →
/// execute()/execute_streaming() 卡死 → run_agentic_loop 死锁(与 env_snapshot::detect 同源 bug)。
/// 本测试外包 10s timeout(远大于 3s 内部超时),无论探测成败都应在 10s 内返回,锁定「不挂死」不变量。
#[cfg(windows)]
#[tokio::test]
async fn probe_pwsh_completes_within_timeout() {
// 10s >> probe_pwsh 内部 3s 超时;若 10s 仍未返回 → 探测挂死,不变量被破坏。
let result = tokio::time::timeout(
std::time::Duration::from_secs(10),
probe_pwsh(),
).await;
assert!(result.is_ok(), "probe_pwsh 必须在 10s 内返回(内部 3s 超时已兜底),不应挂死");
}
}
/// Shell 命令执行请求