From 8fd3ca4a192efe51bb3f2843786ab2a478a298d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=9F=8F=E9=9D=92?= Date: Fri, 5 Jun 2026 14:13:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(less-computer):=20Agent=20=E7=9C=9F?= =?UTF-8?q?=E6=AD=A3=E5=8A=A8=E6=89=8B=E5=B9=B2=E6=B4=BB=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=94=BE=E8=A1=8C+=E6=8A=A4=E6=A0=8F=EF=BC=88acceptEdits=20+?= =?UTF-8?q?=20Bash/=E5=B7=A5=E5=85=B7=20+=20=E9=AB=98=E9=A3=8E=E9=99=A9?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:run_voice_agent_transcript 之前硬编码 Plan 只读 + 无工具,所以 Agent 只能返回文本、 根本不会真的开 Chrome / 跑命令。改成项目既有的「放行 + 护栏」动作模式: - permission_mode = acceptEdits(尊重高级里的设置;coding_agent_mode_from_pref 映射) - allowed_tools = Bash/Read/Edit/Write/Glob/Grep/WebFetch/WebSearch(能真正动手) - 护栏 settings:deny 高风险(rm -rf / sudo / 强推等)自动拦截 = 用户选的「混合:仅高风险」MVP - cwd = workdir 或 $HOME(--add-dir 限定文件作用域);运行前 git 快照便于回滚;用后清理临时护栏文件 真实链路实测(claude v2.1.161,haiku):让它「在 /tmp 建 lesscomputer_probe.txt 内容 ok」 → 文件真的被创建、result=success/「done」、15.8s/$0.05。证明无头链路确实会执行动作。 「开 Chrome」走同一条 Bash 路径(open -a)。 注:交互式弹审批(高风险在弹框点同意)+ 自适应弹框 UI 为后续;当前高风险=自动拦截。 cargo check 0 / less_computer + guard + autonomous_prompt 单测全过。 --- .../src-tauri/src/coordinator/dictation.rs | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index b8ebfdd5..f000d184 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -550,19 +550,59 @@ async fn run_voice_agent_transcript( ); let prefs = inner.prefs.get(); + // 工作目录:用户设的 workdir,否则 $HOME。--add-dir 把文件作用域限定在此。 + let cwd = prefs + .coding_agent_workdir + .clone() + .filter(|d| !d.trim().is_empty()) + .map(std::path::PathBuf::from) + .or_else(|| std::env::var("HOME").ok().map(std::path::PathBuf::from)); + // 运行前 git 快照(cwd 是 git 仓库才有效;非仓库无副作用),便于回滚文件改动。 + if let Some(dir) = &cwd { + if let Some(sha) = crate::coding_agent::create_git_snapshot(dir) { + log::info!("[less-computer] 运行前 git 快照 {sha}(git stash apply 可回滚)"); + } + } + // 「放行 + 护栏」:让 Agent 真正动手(开应用 / 跑命令 / 读写文件)完成任务; + // 高风险/不可逆命令(rm -rf / sudo / 强推等)由护栏 deny 清单自动拦截(混合模式 MVP, + // 高风险=拦截;交互式弹审批为后续)。权限模式尊重高级设置,默认 acceptEdits。 + let mode = coding_agent_mode_from_pref(&prefs.coding_agent_permission_mode); + let settings_json = + crate::coding_agent::guard::build_guard_settings_json(mode.as_cli_arg(), &[]); + let settings_path = std::env::temp_dir().join(format!( + "openless-less-computer-guard-{}.json", + uuid::Uuid::new_v4() + )); + if let Err(e) = std::fs::write( + &settings_path, + serde_json::to_vec_pretty(&settings_json).unwrap_or_default(), + ) { + log::warn!("[less-computer] 写护栏配置失败: {e}"); + } + // 无头单次运行没有多轮兜底:包一层目标驱动的自动化前置说明,逼 Claude 一口气做完。 let prompt = crate::coding_agent::autonomous_prompt(&transcript); - let mut req = crate::coding_agent::CodingAgentRequest::new("voice-agent", prompt); + let mut req = crate::coding_agent::CodingAgentRequest::new("less-computer", prompt); + req.cwd = cwd; req.model = prefs .coding_agent_model .clone() .filter(|m| !m.trim().is_empty()) .or_else(|| Some("sonnet".to_string())); - // 只读 Plan 模式:安全 MVP——返回文本、不产生副作用。后续要工具操作再放开。 - req.permission_mode = crate::coding_agent::CodingAgentPermissionMode::Plan; - req.allowed_tools = Vec::new(); + req.permission_mode = mode; + req.settings_json_path = Some(settings_path.clone()); + req.allowed_tools = vec![ + "Bash".into(), + "Read".into(), + "Edit".into(), + "Write".into(), + "Glob".into(), + "Grep".into(), + "WebFetch".into(), + "WebSearch".into(), + ]; req.max_budget_usd = Some(0.5); - req.timeout_secs = 90; + req.timeout_secs = 120; req.session_persistence = false; let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); @@ -583,6 +623,7 @@ async fn run_voice_agent_transcript( } } let run_result = run.await; + let _ = std::fs::remove_file(&settings_path); let final_text = final_text.trim().to_string(); inner.state.lock().phase = SessionPhase::Idle; @@ -608,6 +649,17 @@ async fn run_voice_agent_transcript( Ok(()) } +/// 把 prefs 里的权限模式字符串映射成枚举;未知值回落到 acceptEdits(放行+护栏的默认)。 +fn coding_agent_mode_from_pref(s: &str) -> crate::coding_agent::CodingAgentPermissionMode { + use crate::coding_agent::CodingAgentPermissionMode as M; + match s.trim() { + "plan" => M::Plan, + "default" => M::Default, + "bypassPermissions" => M::BypassPermissions, + _ => M::AcceptEdits, + } +} + pub(super) fn request_stop_during_starting(inner: &Arc, reason: &str) { { let mut state = inner.state.lock();