重构: aichat agent 能力系统化(L1元能力+L2/L3后端+list去重)
L1 agent 元能力层(治痛①死循环零交付): - env_profile 环境姿势注入 + shell 默认 PowerShell(防引号地狱) - 断路器:同类工具失败≥3熔断 + guard.reset - detect_environment 主动探测工具(python/node/shell) - 求助协议 AiHelpRequired 事件 + 前端求助卡 L2 统一状态机后端(治痛②,前端批2): - ConvState enum 5态 + 合法转换守卫(conv_state.rs) - GeneratingGuard 接入视图层(guard.rs) L3 事件总线后端骨架(治痛③④⑤,接入批2): - EventBus pub-sub + AiBusEvent 8变体(event_bus.rs) list 工具调用重复治理第一步: - build_system_prompt_with_excluded 去重被@实体 + 清单注明语
This commit is contained in:
@@ -1835,6 +1835,140 @@ fn register_file_tools(
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
// ── 环境感知 (Low risk, L1 agent 元能力层) ──
|
||||
// detect_environment:AI 主动探测运行环境的「眼睛」(设计 §2.1 补救式感知)。
|
||||
// 返回 JSON:{ os, default_shell, python_path, node_path }。
|
||||
//
|
||||
// 为什么需要:prompt.rs env_profile_line 只注入静态 OS+shell+解释器提示(编译期 cfg!),
|
||||
// AI 不知 python 真实路径/版本/node 是否安装/GBK 还是 UTF-8,撞墙后才补救。
|
||||
// 本工具让 AI 在「要做某环境相关操作前」(如跑 python 脚本/装依赖)主动探测,
|
||||
// 而非靠 prompt 教(机制优先 prompt 说教,见设计核心原则)。
|
||||
//
|
||||
// 接入断路器(设计 §2.2):后续断路器触发(run_command 连撞 N 次引号/路径错)后,
|
||||
// 系统提示会引导 AI 调本工具刷新环境认知(本工具不主动调断路器,仅提供能力)。
|
||||
//
|
||||
// 安全:Low risk 纯只读探测。which/where 只读 PATH 不写不删,无副作用。
|
||||
// 不走白名单(探测系统 PATH,非 workspace 文件),不审批(只读)。
|
||||
//
|
||||
// 开关(env_probe_enabled,设计 §2.1 §8):DEVFLOW_ENV_PROBE_ENABLED=off 关闭探测,
|
||||
// 返回静态 profile(编译期 OS + 默认 shell,与 env_profile_line 一致),兜底降级旧行为。
|
||||
// 默认 on(空值/未设/on/1/true 均视为开)。开关 off 不致工具调用失败,只退化能力。
|
||||
//
|
||||
// 兜底(设计 §2.1):探测失败(超时/python 未装/which 不可用)→ 该字段 null + errors 收集原因,
|
||||
// 不阻断工具返回。LLM 据此知「该能力不可用」,回退静态 profile 语义。
|
||||
registry.register(
|
||||
"detect_environment",
|
||||
"探测当前运行环境,返回 { os, default_shell, python_path, node_path, errors }。os=Windows/macOS/Linux;default_shell=Windows 默认 PowerShell / Unix 默认 bash(对齐 df-execute ShellType::default);python_path/node_path 为运行时探测到的可执行路径(which/where),探测失败或未安装为 null。只读无副作用。建议在执行 python/node 命令或环境相关操作前主动调用,避免引用不存在的解释器或用错 shell。LLM 可主动调用;命令执行断路器触发后系统提示会引导调用(失败自愈补救式感知)",
|
||||
df_ai::ai_tools::object_schema(vec![]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args: serde_json::Value| Box::pin(async move {
|
||||
// 静态 OS:cfg! 编译期分支,跨设备各自正确(Win/macOS/Linux/Unknown)
|
||||
let os = if cfg!(target_os = "windows") {
|
||||
"Windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macOS"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"Linux"
|
||||
} else {
|
||||
"Unknown"
|
||||
};
|
||||
// 默认 shell:对齐 df_execute::shell::ShellType::default(Windows=PowerShell, Unix=Sh=bash)。
|
||||
// 不直接 import ShellType(避免 dep 漂移),用同样的 cfg! 逻辑保持单一语义源。
|
||||
// 注:Unix 上 ShellType::Sh 实际执行 sh,AI 视角写 bash 兼容脚本即可(POSIX 子集)。
|
||||
let default_shell = if cfg!(target_os = "windows") {
|
||||
"PowerShell"
|
||||
} else {
|
||||
"bash"
|
||||
};
|
||||
|
||||
// 开关 env_probe_enabled(设计 §8):off 则只返静态 profile(无 python_path/node_path 探测),
|
||||
// 兜底降级旧行为(等于 env_profile_line 静态注入的运行时版本)。默认 on。
|
||||
let probe_enabled = match std::env::var("DEVFLOW_ENV_PROBE_ENABLED") {
|
||||
Ok(v) => !matches!(v.trim().to_lowercase().as_str(), "off" | "0" | "false" | "no"),
|
||||
Err(_) => true, // 未设 = 默认开
|
||||
};
|
||||
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
let (python_path, node_path) = if probe_enabled {
|
||||
// Windows 用 where,Unix 用 which。探测命令在 PATH 中不可用或解释器未装时返 null + 记错。
|
||||
// probe_executable 内置 8s 超时兜底(防 which 卡死拖垮会话),失败记 error 不阻断。
|
||||
// Unix python:python3 优先(对齐 env_profile_line 语义),无则试 python(记错降级,不噪音)。
|
||||
let py = if cfg!(target_os = "windows") {
|
||||
probe_executable("python", "where python", &mut errors).await
|
||||
} else {
|
||||
let p3 = probe_executable("python3", "which python3", &mut errors).await;
|
||||
if p3.is_some() {
|
||||
p3
|
||||
} else {
|
||||
// python3 探测失败的 errors 已记入主 errors;python 再探,失败记错(两解释器都无才全 null)。
|
||||
probe_executable("python", "which python", &mut errors).await
|
||||
}
|
||||
};
|
||||
let node = if cfg!(target_os = "windows") {
|
||||
probe_executable("node", "where node", &mut errors).await
|
||||
} else {
|
||||
probe_executable("node", "which node", &mut errors).await
|
||||
};
|
||||
(py, node)
|
||||
} else {
|
||||
// 开关关:探测降级,两字段 null(等于不探测)。errors 不记(非失败,是开关显式关闭)。
|
||||
(None, None)
|
||||
};
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"os": os,
|
||||
"default_shell": default_shell,
|
||||
"python_path": python_path,
|
||||
"node_path": node_path,
|
||||
"probe_enabled": probe_enabled,
|
||||
"errors": errors,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/// 探测可执行文件路径(which/where 风格),8s 超时兜底。
|
||||
///
|
||||
/// 执行 `cmd`(如 "which python3" / "where node")取 stdout 首行(trim 后)作为路径。
|
||||
/// 失败(命令不存在/解释器未装/超时/退出码非 0)记 `error` 到 `errors` 并返回 None。
|
||||
///
|
||||
/// 超时兜底(对齐 workflow-cargo-timeout-wrap 记忆):防 which/where 偶发卡死拖垮会话。
|
||||
/// 用 df_execute::shell::execute(对齐 run_command 同源,Windows 无控制台窗口 + kill_on_drop),
|
||||
/// shell_type 用 default(Windows=PowerShell / Unix=sh)。
|
||||
async fn probe_executable(label: &str, cmd: &str, errors: &mut Vec<String>) -> Option<String> {
|
||||
let request = ShellRequest {
|
||||
command: cmd.to_string(),
|
||||
working_dir: None,
|
||||
env: HashMap::new(),
|
||||
// 8s 超时:which/where 通常 <1s,8s 足够且不拖垮会话。超时视为不可用(记错返 None)。
|
||||
timeout_secs: Some(8),
|
||||
shell_type: Default::default(),
|
||||
};
|
||||
match execute(request).await {
|
||||
Ok(result) => {
|
||||
// 退出码非 0:which/where 未找到目标(如 python 未装)→ 正常情况,记错返 None
|
||||
if !matches!(result.exit_code, Some(0)) {
|
||||
errors.push(format!(
|
||||
"{}: 探测命令退出码 {:?}({})",
|
||||
label, result.exit_code, cmd
|
||||
));
|
||||
return None;
|
||||
}
|
||||
let first_line = result.stdout.lines().next().map(|s| s.trim().to_string());
|
||||
match first_line {
|
||||
Some(p) if !p.is_empty() => Some(p),
|
||||
_ => {
|
||||
errors.push(format!("{}: 探测输出为空({})", label, cmd));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(format!("{}: 探测失败({}): {}", label, cmd, e));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归列出目录内容(最多 max_depth 层,最多 max_entries 条)
|
||||
@@ -2262,7 +2396,7 @@ mod tests {
|
||||
// 任一层漏移 register 调用,此测试立即红。工具名集合也断言,防 rename 致 LLM tool 突变。
|
||||
// ============================================================
|
||||
|
||||
/// build_ai_tool_registry 应注册恰好 30 个工具(18 data + 11 file + 1 http),且工具名集合稳定。
|
||||
/// build_ai_tool_registry 应注册恰好 31 个工具(18 data + 12 file + 1 http),且工具名集合稳定。
|
||||
///
|
||||
/// 用 in-memory SQLite(Database::open_in_memory 自跑迁移),构造零外部依赖的 db,
|
||||
// 不实际执行任何 handler——仅断言注册阶段的定义完整性,故无需真实数据。
|
||||
@@ -2275,17 +2409,18 @@ mod tests {
|
||||
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs::default_with_root()));
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs);
|
||||
|
||||
// 总量基线:30(18 data + 11 file + 1 http)。拆分前后必须一致。
|
||||
// 总量基线:31(18 data + 12 file + 1 http)。拆分前后必须一致。
|
||||
// F-260621: file 层 10→11(新增 grep 跨文件内容搜索工具)。
|
||||
// L1 环境感知(设计 §2.1): file 层 11→12(新增 detect_environment 环境探测工具)。
|
||||
assert_eq!(
|
||||
registry.len(),
|
||||
30,
|
||||
"工具总数应为 30(18 data + 11 file + 1 http),实际 {}", registry.len()
|
||||
31,
|
||||
"工具总数应为 31(18 data + 12 file + 1 http),实际 {}", registry.len()
|
||||
);
|
||||
|
||||
// 工具名集合基线:防 rename / 漏注册 / 误删除。
|
||||
// data 层 18 个(持 db):CRUD/状态机/工作流
|
||||
// file 层 11 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜/grep
|
||||
// file 层 12 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜/grep/环境探测
|
||||
// http 层 1 个(不持 db):http_request
|
||||
let mut expected: Vec<&str> = vec![
|
||||
// ── data 层 (18) ──
|
||||
@@ -2295,12 +2430,13 @@ mod tests {
|
||||
"run_workflow", "delete_task", "create_idea",
|
||||
"delete_project", "restore_project", "purge_project",
|
||||
"list_trash", "get_project_count", "get_task_count",
|
||||
// ── file 层 (11) ──(run_command 注册顺序已移至末位降低 LLM 偏好,
|
||||
// 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621)
|
||||
// ── file 层 (12) ──(run_command 注册顺序已移至末位降低 LLM 偏好,
|
||||
// 集合断言经 sort 后与顺序无关,仅守护工具名不漂移。grep 新增 F-260621;
|
||||
// detect_environment 新增 L1 环境感知 设计 §2.1)
|
||||
"read_file", "list_directory", "write_file",
|
||||
"patch_file", "file_info", "append_file",
|
||||
"delete_file", "rename_file", "search_files", "run_command",
|
||||
"grep",
|
||||
"grep", "detect_environment",
|
||||
// ── http 层 (1) ──
|
||||
"http_request",
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user