新增: idea source 消息级溯源 + workspace_root 去固定根 + run_command 偏好修复
idea source 结构化(消息级溯源闭环 — 灵感来源 conv_msg:{id}):
- 新建 idea_source.rs(IDEA_SOURCE_AUTO_FILL_ENABLED + maybe_fill_idea_source 保守五条件:
开关/create_idea/source 空(不覆盖 AI 填)/message_id 非 None/result.id)
- audit/mod low_risk + chat ai_approve 双执行路径接补全(create_idea Medium 审批)
workspace_root 去固定根(F-260619-03 收尾):
- is_authorized 删 workspace_root 固定放行(代码硬编码),改走 persistent/session 白名单
- default_with_root 保留(默认初始 persistent,首次免授权行为不变)
- 用户从白名单删工程根后也需授权(动态白名单完整语义)
run_command 偏好修复(db 分析:591ff4a0 run_command 31/56=55% 严重偏好):
- P1 描述收紧(明确仅命令执行 + 负向引导:读取 read_file/编辑 patch_file/列目录 list_directory/搜索 search_files)
- P2 ToolDomain::Exec(run_command 独立 domain,仅 Debug 含 Exec,Code/File/Search 不含)
- P3 注册顺序(run_command 放 File 层最后)+ DEBUG_RULES 加命令执行关键词
自验: df-ai 228(intent Exec)+ devflow 169(idea source)+ workspace EXIT 0
This commit is contained in:
@@ -881,73 +881,6 @@ fn register_file_tools(
|
||||
// run_command 不走白名单(High risk 靠人工审批兜底,放开目录让 AI 在用户任意项目目录闭环),
|
||||
// 故该工具闭包不捕获 allowed_dirs,维持原行为。
|
||||
let allowed_dirs = allowed_dirs.clone();
|
||||
registry.register(
|
||||
"run_command", "在指定工作目录执行 shell 命令(跑测试/构建/查看运行结果),返回 stdout/stderr/exit_code。高风险,须人工批准。命令需自包含(非交互式,避免需用户输入的程序)。默认超时 60 秒。用于验证刚写入的代码能否运行、跑测试、看报错迭代修改。",
|
||||
df_ai::ai_tools::object_schema(vec![
|
||||
("command", "string", true),
|
||||
("working_dir", "string", false),
|
||||
("timeout_secs", "integer", false),
|
||||
]),
|
||||
RiskLevel::High,
|
||||
Box::new(|args: serde_json::Value| Box::pin(async move {
|
||||
let command = args["command"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 command 参数"))?;
|
||||
// working_dir 默认 workspace_root()(与 write_file 锚定一致:AI 写代码→同目录跑命令,闭环)。
|
||||
// 走 validate_path 黑名单(.. + 敏感系统目录)作基础防线;不走 resolve_workspace_path 越界校验——
|
||||
// run_command 是 High risk 靠人工审批兜底(用户在审批卡看清 command+working_dir),
|
||||
// 放开目录才能让 AI 在用户任意项目目录形成「写→跑→看→改」真闭环。
|
||||
let working_dir = match args.get("working_dir").and_then(|v| v.as_str()) {
|
||||
Some(d) => {
|
||||
validate_path(d)?;
|
||||
d.to_string()
|
||||
}
|
||||
None => workspace_root().to_string_lossy().to_string(),
|
||||
};
|
||||
// timeout 默认 60s:防 hang(交互式命令/死循环/大构建),LLM 可通过 args timeout_secs 覆盖
|
||||
// clamp 封顶 MAX:防 LLM 传超大 timeout_secs 冻结会话(需更长命令应拆分而非无限等)
|
||||
let timeout_secs = args["timeout_secs"].as_u64().unwrap_or(DEFAULT_RUN_COMMAND_TIMEOUT_SECS).min(MAX_RUN_COMMAND_TIMEOUT_SECS);
|
||||
|
||||
let request = ShellRequest {
|
||||
command: command.to_string(),
|
||||
working_dir: Some(working_dir.clone()),
|
||||
env: HashMap::new(),
|
||||
timeout_secs: Some(timeout_secs),
|
||||
shell_type: Default::default(),
|
||||
};
|
||||
// F-260616-04:超时标注——execute 超时返 Err("命令执行超时: N秒")。
|
||||
// 原行为:该 Err 经 ? 上抛 → 人工审批路径(commands.rs ai_approve L256)把 e.to_string()
|
||||
// 包成 tool_result 回传 LLM → LLM 误判命令失败而非超时 → 盲目重试同命令 →
|
||||
// 新 tool_call_id → 重新 insert pending → 重新审批,「再过一会又提示 Run Command」循环。
|
||||
// 治本:超时根因处拦截,把 Err 内容改写为「明确超时语义 + 勿盲目重试」标注,
|
||||
// 让 LLM 知进程已终止、非命令失败,确需更长时限才在 args 提高 timeout_secs 重发。
|
||||
let result = execute(request).await.map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("命令执行超时") {
|
||||
anyhow::anyhow!(
|
||||
"命令执行超时({}s),进程已终止。勿盲目重试同命令;确需更长时限重发时在 args 提高 timeout_secs。",
|
||||
timeout_secs
|
||||
)
|
||||
} else {
|
||||
e
|
||||
}
|
||||
})?;
|
||||
|
||||
// 输出截断:防编译输出/find//cat 大文件撑爆 LLM context(各 10KB,尾部保留-报错堆栈在末尾)
|
||||
const MAX_OUT: usize = 10_000;
|
||||
let (stdout, stdout_trunc) = truncate_output(&result.stdout, MAX_OUT);
|
||||
let (stderr, stderr_trunc) = truncate_output(&result.stderr, MAX_OUT);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"command": command,
|
||||
"working_dir": working_dir,
|
||||
"exit_code": result.exit_code,
|
||||
"duration_ms": result.duration_ms,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
"truncated": stdout_trunc || stderr_trunc,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
// ── 文件系统 ──
|
||||
registry.register(
|
||||
@@ -1633,6 +1566,76 @@ fn register_file_tools(
|
||||
})
|
||||
})},
|
||||
);
|
||||
|
||||
// ── 命令执行(run_command 放 File 层最后,降低 LLM 偏好:靠前注册易被 LLM 优先选中,
|
||||
// 移到末位让 read_file/write_file 等高频文件工具在前,run_command 仅命令执行场景才该用) ──
|
||||
registry.register(
|
||||
"run_command", "在指定工作目录执行 shell 命令,返回 stdout/stderr/exit_code。仅用于命令执行场景:跑测试套件、构建项目、运行二进制/脚本验证行为。读取文件用 read_file,编辑文件用 patch_file/write_file,列目录用 list_directory,搜索文件名用 search_files——不要用本工具完成这些操作。高风险,须人工批准。命令需自包含(非交互式,避免需用户输入的程序)。默认超时 60 秒。",
|
||||
df_ai::ai_tools::object_schema(vec![
|
||||
("command", "string", true),
|
||||
("working_dir", "string", false),
|
||||
("timeout_secs", "integer", false),
|
||||
]),
|
||||
RiskLevel::High,
|
||||
Box::new(|args: serde_json::Value| Box::pin(async move {
|
||||
let command = args["command"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 command 参数"))?;
|
||||
// working_dir 默认 workspace_root()(与 write_file 锚定一致:AI 写代码→同目录跑命令,闭环)。
|
||||
// 走 validate_path 黑名单(.. + 敏感系统目录)作基础防线;不走 resolve_workspace_path 越界校验——
|
||||
// run_command 是 High risk 靠人工审批兜底(用户在审批卡看清 command+working_dir),
|
||||
// 放开目录才能让 AI 在用户任意项目目录形成「写→跑→看→改」真闭环。
|
||||
let working_dir = match args.get("working_dir").and_then(|v| v.as_str()) {
|
||||
Some(d) => {
|
||||
validate_path(d)?;
|
||||
d.to_string()
|
||||
}
|
||||
None => workspace_root().to_string_lossy().to_string(),
|
||||
};
|
||||
// timeout 默认 60s:防 hang(交互式命令/死循环/大构建),LLM 可通过 args timeout_secs 覆盖
|
||||
// clamp 封顶 MAX:防 LLM 传超大 timeout_secs 冻结会话(需更长命令应拆分而非无限等)
|
||||
let timeout_secs = args["timeout_secs"].as_u64().unwrap_or(DEFAULT_RUN_COMMAND_TIMEOUT_SECS).min(MAX_RUN_COMMAND_TIMEOUT_SECS);
|
||||
|
||||
let request = ShellRequest {
|
||||
command: command.to_string(),
|
||||
working_dir: Some(working_dir.clone()),
|
||||
env: HashMap::new(),
|
||||
timeout_secs: Some(timeout_secs),
|
||||
shell_type: Default::default(),
|
||||
};
|
||||
// F-260616-04:超时标注——execute 超时返 Err("命令执行超时: N秒")。
|
||||
// 原行为:该 Err 经 ? 上抛 → 人工审批路径(commands.rs ai_approve L256)把 e.to_string()
|
||||
// 包成 tool_result 回传 LLM → LLM 误判命令失败而非超时 → 盲目重试同命令 →
|
||||
// 新 tool_call_id → 重新 insert pending → 重新审批,「再过一会又提示 Run Command」循环。
|
||||
// 治本:超时根因处拦截,把 Err 内容改写为「明确超时语义 + 勿盲目重试」标注,
|
||||
// 让 LLM 知进程已终止、非命令失败,确需更长时限才在 args 提高 timeout_secs 重发。
|
||||
let result = execute(request).await.map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("命令执行超时") {
|
||||
anyhow::anyhow!(
|
||||
"命令执行超时({}s),进程已终止。勿盲目重试同命令;确需更长时限重发时在 args 提高 timeout_secs。",
|
||||
timeout_secs
|
||||
)
|
||||
} else {
|
||||
e
|
||||
}
|
||||
})?;
|
||||
|
||||
// 输出截断:防编译输出/find//cat 大文件撑爆 LLM context(各 10KB,尾部保留-报错堆栈在末尾)
|
||||
const MAX_OUT: usize = 10_000;
|
||||
let (stdout, stdout_trunc) = truncate_output(&result.stdout, MAX_OUT);
|
||||
let (stderr, stderr_trunc) = truncate_output(&result.stderr, MAX_OUT);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"command": command,
|
||||
"working_dir": working_dir,
|
||||
"exit_code": result.exit_code,
|
||||
"duration_ms": result.duration_ms,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
"truncated": stdout_trunc || stderr_trunc,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/// 递归列出目录内容(最多 max_depth 层,最多 max_entries 条)
|
||||
@@ -1852,10 +1855,11 @@ mod tests {
|
||||
"run_workflow", "delete_task", "create_idea",
|
||||
"delete_project", "restore_project", "purge_project",
|
||||
"list_trash", "get_project_count", "get_task_count",
|
||||
// ── file 层 (10) ──
|
||||
"run_command", "read_file", "list_directory",
|
||||
"write_file", "patch_file", "file_info",
|
||||
"append_file", "delete_file", "rename_file", "search_files",
|
||||
// ── file 层 (10) ──(run_command 注册顺序已移至末位降低 LLM 偏好,
|
||||
// 集合断言经 sort 后与顺序无关,仅守护工具名不漂移)
|
||||
"read_file", "list_directory", "write_file",
|
||||
"patch_file", "file_info", "append_file",
|
||||
"delete_file", "rename_file", "search_files", "run_command",
|
||||
// ── http 层 (1) ──
|
||||
"http_request",
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user