diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs index d53d18a22..bedde75b8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs @@ -159,7 +159,7 @@ fn configure_sidecar_command( .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); - configure_claude_code_environment(command, llm, home); + configure_claude_code_environment(command, llm, home)?; configure_claude_code_process(command); Ok(()) } @@ -466,7 +466,6 @@ pub(crate) fn game_creator_claude_code_cli_version_identity() -> Result Result, isolated_home: &Path, -) { +) -> Result<(), String> { command.env_clear(); for name in [ "PATH", @@ -641,7 +652,8 @@ fn configure_claude_code_environment( copy_env(command, name); } let isolated_claude_home = isolated_home.join("claude"); - let _ = std::fs::create_dir_all(&isolated_claude_home); + std::fs::create_dir_all(&isolated_claude_home) + .map_err(|error| format!("准备 Claude Code 隔离目录失败:{error}"))?; command .env("HOME", isolated_home) // Windows 上 Node 的 `os.homedir()` 只看 `USERPROFILE`;不隔离它,Claude Code 会去读 @@ -689,6 +701,7 @@ fn configure_claude_code_environment( command.env("ANTHROPIC_MODEL", llm.model.trim()); } } + Ok(()) } fn claude_result_error_detail(value: &serde_json::Value) -> Option { @@ -2146,7 +2159,7 @@ mod tests { fn sidecar_environment_is_isolated_from_the_local_claude_code_home() { let home = std::env::temp_dir().join("agc-cc-isolated-home-test"); let mut command = tokio::process::Command::new("node"); - configure_claude_code_environment(&mut command, None, &home); + configure_claude_code_environment(&mut command, None, &home).unwrap(); let envs = command .as_std() .get_envs() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs index e203810b6..36777bb12 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs @@ -370,7 +370,7 @@ impl ExecutionAdapter { Ok::<_, String>((session.root.clone(), session)) }) .await - .map_err(|_| "宿主执行身份读取中断")??; + .map_err(|error| format!("宿主执行身份读取中断:{error}"))??; Ok(Self::new( root, session, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index 41f9d248b..1add77002 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -876,13 +876,13 @@ pub(super) fn resolve_direct_codex_project_authority( return Err("AGC 直连项目根目录必须是绝对路径".to_string()); } let project_metadata = std::fs::metadata(project_root) - .map_err(|_| "AGC 直连项目根目录不存在或无法读取".to_string())?; + .map_err(|error| format!("AGC 直连项目根目录不存在或无法读取:{error}"))?; if !project_metadata.is_dir() { return Err("AGC 直连项目根目录不是目录".to_string()); } let project_root = project_root .canonicalize() - .map_err(|_| "AGC 直连项目根目录无法安全解析".to_string())?; + .map_err(|error| format!("AGC 直连项目根目录无法安全解析:{error}"))?; // 用户选择的项目目录就是 Codex 工作区根,不再强制要求 game/ 子目录。 // 原生文件工具仍由项目文件层拒绝 .agent/**、.git/**、密钥等控制面路径。 Ok((project_root.clone(), project_root)) @@ -2597,8 +2597,10 @@ impl CodexAppServerConnection { platform_llm::LlmError::InvalidRequest("Direct 连接缺少项目目录".into()) })? .canonicalize() - .map_err(|_| { - platform_llm::LlmError::InvalidRequest("Direct 项目目录不可用".into()) + .map_err(|error| { + platform_llm::LlmError::InvalidRequest(format!( + "Direct 项目目录不可用:{error}" + )) })?; let bind_root = root.clone(); let bind_executable = executable.clone(); @@ -2612,7 +2614,9 @@ impl CodexAppServerConnection { session.bind_codex_executor(&bind_executable, &bind_version) }) .await - .map_err(|_| platform_llm::LlmError::InvalidRequest("Direct 执行器绑定中断".into()))? + .map_err(|error| { + platform_llm::LlmError::InvalidRequest(format!("Direct 执行器绑定中断:{error}")) + })? .map_err(platform_llm::LlmError::InvalidRequest)?; Some(root) } else { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs index 059daae23..7df7453c2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs @@ -397,7 +397,7 @@ fn closed_error(phase: ExecutionPhase) -> String { pub(super) fn current(root: &Path) -> Result, String> { let root = root .canonicalize() - .map_err(|_| "direct-execution-project: 项目不可用")?; + .map_err(|error| format!("direct-execution-project: 项目不可用:{error}"))?; sessions() .lock() .map_err(|_| "direct-execution-state: 会话状态不可用")? diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index 7b342fc4a..1d35e585e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -3990,7 +3990,10 @@ async fn handle_direct_tool_bridge( let session = super::direct_execution::current(&root)?; let has_evidence = !session.snapshot()?.evidence.is_empty(); let before = if has_evidence { - super::direct_validation::source_fingerprint(&root).ok() + Some( + super::direct_validation::source_fingerprint(&root) + .map_err(|cause| format!("执行前成果指纹读取失败:{cause}"))?, + ) } else { None }; @@ -4213,10 +4216,18 @@ async fn handle_direct_tool_bridge( let root = state.root.clone(); let passed = result.is_ok(); let finished = tokio::task::spawn_blocking(move || { - let changed = has_evidence - && before - .zip(super::direct_validation::source_fingerprint(&root).ok()) - .is_none_or(|(before, after)| before != after); + let changed = if has_evidence { + match before { + Some(before) => { + let after = super::direct_validation::source_fingerprint(&root) + .map_err(|cause| format!("执行后成果指纹读取失败:{cause}"))?; + before != after + } + None => false, + } + } else { + false + }; if !passed && dispatch_denied { lease.finish_paid_dispatch_denied(changed) } else { @@ -4224,14 +4235,19 @@ async fn handle_direct_tool_bridge( } }) .await; - if !matches!(finished, Ok(Ok(()))) { + let receipt_error = match finished { + Ok(Ok(())) => None, + Ok(Err(error)) => Some(format!("执行回执结算失败:{error}")), + Err(error) => Some(format!("执行回执结算任务未返回:{error}")), + }; + if let Some(cause) = receipt_error { return Json(compose_direct_tool_outcome( &state, request.tool.as_str(), &diagnostics_arguments, dispatch_denied, Err(ToolCallError::from( - &DirectExecutionGateRejection::ReceiptNotPersisted, + &DirectExecutionGateRejection::ReceiptNotPersisted { cause }, )), )); } @@ -4270,7 +4286,7 @@ async fn start_tool_bridge_for_source( } let root = root .canonicalize() - .map_err(|_| "AGC 工具桥项目目录无法安全解析".to_string())?; + .map_err(|error| format!("AGC 工具桥项目目录无法安全解析:{error}"))?; let route = format!("/tool-{}", uuid::Uuid::new_v4().simple()); let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)) .await diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs index dde3bd93d..eee83fb55 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs @@ -1186,17 +1186,22 @@ fn validate_external_mcp_record_arguments( fn read_external_mcp_journal(root: &Path) -> Result, String> { let path = external_mcp_journal_path(root); - let Ok(bytes) = std::fs::read(&path) else { - return Ok(Vec::new()); + let bytes = match std::fs::read(&path) { + Ok(bytes) => bytes, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => return Err(format!("读取 Codex 返回记录失败:{error}")), }; if bytes.len() as u64 > EXTERNAL_MCP_JOURNAL_MAX_BYTES { return Err("Codex 返回记录超过客户端保留上限".to_string()); } bytes .split(|byte| *byte == b'\n') - .filter(|line| !line.is_empty()) - .map(|line| { - serde_json::from_slice::(line).map_err(|_| "Codex 返回记录格式损坏".to_string()) + .enumerate() + .filter(|(_, line)| !line.is_empty()) + .map(|(line_number, line)| { + serde_json::from_slice::(line).map_err(|error| { + format!("Codex 返回记录格式损坏:第 {} 行:{error}", line_number + 1) + }) }) .collect() } @@ -1504,15 +1509,20 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option< if uri != "agc://conversation/codex-responses" { Some(mcp_error(id, -32602, "未知资源")) } else { - let text = read_external_mcp_journal(root) - .map(|records| { - records - .iter() - .map(Value::to_string) - .collect::>() - .join("\n") - }) - .unwrap_or_default(); + let text = match read_external_mcp_journal(root) { + Ok(records) => records + .iter() + .map(Value::to_string) + .collect::>() + .join("\n"), + Err(error) => { + return Some(mcp_error( + id, + -32603, + &format!("Codex 返回记录资源读取失败:{error}"), + )); + } + }; if text.len() > DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES { return Some(mcp_error(id, -32000, "Codex 返回记录资源超过响应大小上限")); } @@ -3434,6 +3444,26 @@ mod tests { .is_err()); } + #[test] + fn external_codex_response_journal_distinguishes_missing_from_read_and_parse_errors() { + let temporary = crate::tests::canonical_test_tempdir("direct-tools-read-response-"); + let root = temporary.path(); + init_local_game_project_at(root, "direct-tools-read-response", "Codex 返回读取测试") + .expect("init project"); + assert!(read_external_mcp_journal(root).unwrap().is_empty()); + + let journal = external_mcp_journal_path(root); + std::fs::create_dir_all(&journal).unwrap(); + let error = read_external_mcp_journal(root).unwrap_err(); + assert!(error.contains("读取 Codex 返回记录失败"), "{error}"); + + std::fs::remove_dir_all(&journal).unwrap(); + std::fs::write(&journal, b"{broken-json\n").unwrap(); + let error = read_external_mcp_journal(root).unwrap_err(); + assert!(error.contains("第 1 行"), "{error}"); + assert!(error.contains("Codex 返回记录格式损坏"), "{error}"); + } + #[test] fn recorded_codex_response_only_writes_its_own_journal() { let temporary = crate::tests::canonical_test_tempdir("direct-tools-record-response-"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/tool/error.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/tool/error.rs index 514ec8838..757ca7c4a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/tool/error.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/tool/error.rs @@ -259,8 +259,8 @@ pub(crate) enum DirectExecutionGateRejection { SessionUnavailable { cause: String }, /// 取执行会话的阻塞任务没有返回;保留 join/panic 事实,避免退成无因固定句。 SessionTaskLost { cause: String }, - /// 工具已返回,但执行回执没有可靠落盘。 - ReceiptNotPersisted, + /// 工具已返回,但执行回执没有可靠落盘;保留落盘或回执任务的具体原因。 + ReceiptNotPersisted { cause: String }, } impl ToolFailure for DirectExecutionGateRejection { @@ -276,10 +276,9 @@ impl ToolFailure for DirectExecutionGateRejection { Self::SessionTaskLost { cause } => { format!("宿主执行会话任务未返回:{cause}") } - Self::ReceiptNotPersisted => { - "工具已返回,但宿主执行回执未可靠落盘;请查看交付状态并核对原操作,不自动重放" - .to_string() - } + Self::ReceiptNotPersisted { cause } => format!( + "工具已返回,但宿主执行回执未可靠落盘:{cause};请查看交付状态并核对原操作,不自动重放" + ), } } } diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index f373aef22..adb112298 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -86,3 +86,5 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - 错误报告对话框读取最新错误事件或诊断日志失败时原先只显示“暂不可用/使用快照”;现把读取 IPC/文件正文附在状态提示中,快照回退行为保持不变。 - WorkspaceLauncher 的清单版本读取、失效事件重读和事件订阅失败原先只写日志或静默回退;现把安全正文送入启动器状态提示,仍保留已有回退刷新路径。 - Direct 补丁事务的目标指纹读取原先把元数据、打开、正文读取和文件超限都折叠为 `None`,可能误判文件缺失或继续比较;现只对确实 `NotFound` 保留 `missing`,其余失败返回目标相对路径、阶段和底层正文,避免补丁结果被错误归因。 +- CC/Direct 工具桥的执行前后成果指纹、回执结算 JoinError/落盘失败,以及外部 MCP journal 的读取/JSON 行解析原先仍会被 `.ok()`、固定回执句或“无记录”吞掉;现分别保留指纹阶段、回执任务/落盘原因、journal 行号和底层正文,只有真实 `NotFound` 才视为空记录。 +- 外部 MCP `resources/read` 仍把已能返回具体原因的 Codex journal 读取错误 `unwrap_or_default()` 成空资源;现把读取失败按 MCP 错误响应返回,Direct 执行会话当前项目 canonicalize 也保留底层 I/O 正文。