From 8710de7bdeb321b12538aa61cf01132b0e9395d3 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:31:40 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E6=94=B6=E6=95=9B=20app-serv?= =?UTF-8?q?er=20=E8=BF=9B=E7=A8=8B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保留执行器核验、OAuth、回合身份与图片解码正文 保留进程树归属、Job、等待和终止原因 补充进程树与 base64 回归验证 --- .../src/agent/codex_app_server/mod.rs | 30 +++++++++--- .../agent/codex_app_server/process_tree.rs | 48 +++++++++++++------ .../src/agent/direct_project_context.rs | 4 +- ...€�里程碑】AGC错误具体文本展示-2026-10-04.md | 1 + 4 files changed, 60 insertions(+), 23 deletions(-) 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 1add77002..39b0d7fc1 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 @@ -1780,8 +1780,8 @@ async fn stage_codex_app_server_image( let (extension, encoded) = image_data_url_parts(image_url)?; let bytes = base64::engine::general_purpose::STANDARD .decode(encoded) - .map_err(|_| { - platform_llm::LlmError::InvalidRequest("多模态图片 base64 内容无效".to_string()) + .map_err(|error| { + platform_llm::LlmError::InvalidRequest(format!("多模态图片 base64 内容无效:{error}")) })?; if bytes.is_empty() || bytes.len() > GAME_CREATOR_CODEX_APP_SERVER_IMAGE_MAX_BYTES { return Err(platform_llm::LlmError::InvalidRequest( @@ -2527,7 +2527,9 @@ impl CodexAppServerConnection { Ok::<_, String>((executable, version)) }) .await - .map_err(|_| platform_llm::LlmError::InvalidConfig("Codex 执行器身份核验中断".into()))? + .map_err(|error| { + platform_llm::LlmError::InvalidConfig(format!("Codex 执行器身份核验中断:{error}")) + })? .map_err(platform_llm::LlmError::InvalidConfig)?; if workspace_mode == CodexAppServerWorkspaceMode::DirectProject { execution::validate_approval_version(&codex_cli_version) @@ -3066,8 +3068,10 @@ impl CodexAppServerConnection { let private_home = isolated_codex_home.clone(); tokio::task::spawn_blocking(move || handoff.remember(&private_home)) .await - .map_err(|_| { - platform_llm::LlmError::InvalidConfig("私有 OAuth 轮换状态保存中断".into()) + .map_err(|error| { + platform_llm::LlmError::InvalidConfig(format!( + "私有 OAuth 轮换状态保存中断:{error}" + )) })? .map_err(platform_llm::LlmError::InvalidConfig)?; } @@ -5501,9 +5505,9 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer( .map(|state| state.client_turn_id) }) .await - .map_err(|_| { + .map_err(|error| { TurnError::HostStateUnavailable(HostStateUnavailable { - detail: "宿主 CLI 回合身份读取中断".to_string(), + detail: format!("宿主 CLI 回合身份读取中断:{error}"), }) })? .map_err(|detail| TurnError::HostStateUnavailable(HostStateUnavailable { detail }))?; @@ -6650,6 +6654,18 @@ mod tests { assert!(std::path::Path::new(staged_path).is_file()); } + #[tokio::test] + async fn app_server_invalid_base64_preserves_decoder_detail() { + let temp = tempfile::tempdir().expect("temp dir"); + let error = + stage_codex_app_server_image(temp.path(), "data:image/png;base64,not-base64", 0) + .await + .unwrap_err() + .to_string(); + assert!(error.contains("多模态图片 base64 内容无效"), "{error}"); + assert!(error.contains("Invalid"), "{error}"); + } + #[test] fn app_server_multimodal_validation_rejects_system_images() { let request = LlmRunRequest::new(vec![LlmMessage::multimodal( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/process_tree.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/process_tree.rs index 5a234b37d..e83fb8333 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/process_tree.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/process_tree.rs @@ -46,15 +46,15 @@ impl OwnedProcessTree { #[cfg(unix)] let start_identity = crate::process_identity::external_agent_runner_process_start_identity(pid) - .map_err(|_| "app-server-process-identity-unknown")? + .map_err(|error| format!("app-server-process-identity-unknown:{error}"))? .filter(|value| !value.is_empty()) - .ok_or("app-server-process-identity-unknown")?; + .ok_or_else(|| "app-server-process-identity-unknown:启动身份为空".to_string())?; #[cfg(windows)] let job = crate::process_session::WindowsProcessJob::assign_tokio_named( child, &format!("Local\\AGCCodex-{}", uuid::Uuid::new_v4()), ) - .map_err(|_| "app-server-process-job-unavailable")?; + .map_err(|error| format!("app-server-process-job-unavailable:{error}"))?; #[cfg(not(any(windows, unix)))] return Err("app-server-process-control-unsupported".into()); #[cfg(any(windows, unix))] @@ -91,16 +91,26 @@ impl OwnedProcessTree { return Err("app-server-process-owner-mismatch".into()); } let deadline = tokio::time::Instant::now() + STOP_TIMEOUT; - if self.terminate_owned_tree().is_err() { + if let Err(error) = self.terminate_owned_tree() { // 只清理仍持有的直属 Child 句柄;不能据此报告子树已回收。 - let _ = child.start_kill(); - let _ = tokio::time::timeout_at(deadline, child.wait()).await; - return Err("app-server-process-tree-termination-uncertain".into()); + let kill_error = child + .start_kill() + .err() + .map(|error| format!(";直属进程终止失败:{error}")) + .unwrap_or_default(); + let wait_error = match tokio::time::timeout_at(deadline, child.wait()).await { + Ok(Ok(_)) => String::new(), + Ok(Err(error)) => format!(";等待直属进程退出失败:{error}"), + Err(_) => ";等待直属进程退出超时".to_string(), + }; + return Err(format!( + "app-server-process-tree-termination-uncertain:{error}{kill_error}{wait_error}" + )); } tokio::time::timeout_at(deadline, child.wait()) .await - .map_err(|_| "app-server-process-exit-timeout")? - .map_err(|_| "app-server-process-exit-unconfirmed")?; + .map_err(|_| "app-server-process-exit-timeout:等待直属进程退出超过 5 秒".to_string())? + .map_err(|error| format!("app-server-process-exit-unconfirmed:{error}"))?; loop { if self.observed_tree_empty()? { break; @@ -127,7 +137,7 @@ impl OwnedProcessTree { { self.job .is_empty() - .map_err(|_| "app-server-process-tree-state-unknown".into()) + .map_err(|error| format!("app-server-process-tree-state-unknown:{error}")) } #[cfg(unix)] { @@ -141,7 +151,10 @@ impl OwnedProcessTree { if std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) { Ok(true) } else { - Err("app-server-process-tree-state-unknown".into()) + Err(format!( + "app-server-process-tree-state-unknown:{}", + std::io::Error::last_os_error() + )) } } #[cfg(not(any(windows, unix)))] @@ -155,7 +168,7 @@ impl OwnedProcessTree { { self.job .terminate() - .map_err(|_| "app-server-process-tree-termination-uncertain".into()) + .map_err(|error| format!("app-server-process-tree-termination-uncertain:{error}")) } #[cfg(unix)] { @@ -165,7 +178,7 @@ impl OwnedProcessTree { // leader 已消失或 PID 被复用时不盲杀 PGID;保留不确定状态。 let current = crate::process_identity::external_agent_runner_process_start_identity(self.pid) - .map_err(|_| "app-server-process-identity-unknown")?; + .map_err(|error| format!("app-server-process-identity-unknown:{error}"))?; if current.as_deref() != Some(self.start_identity.as_str()) { return Err("app-server-process-owner-mismatch".into()); } @@ -173,7 +186,10 @@ impl OwnedProcessTree { if result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) { Ok(()) } else { - Err("app-server-process-tree-termination-uncertain".into()) + Err(format!( + "app-server-process-tree-termination-uncertain:{}", + std::io::Error::last_os_error() + )) } } #[cfg(not(any(windows, unix)))] @@ -186,7 +202,9 @@ impl OwnedProcessTree { impl Drop for OwnedProcessTree { fn drop(&mut self) { // Drop 只做应急回收,永远不伪造完成证明;正式终态必须 await shutdown。 - let _ = self.terminate_owned_tree(); + if let Err(error) = self.terminate_owned_tree() { + app_log!("agent.codex_app_server.process_tree.drop_cleanup_failed error={error}"); + } } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs index 42931a2f0..b4c30424b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs @@ -52,7 +52,9 @@ struct ContextIdentity { fn context_identity(root: &Path) -> Result { let manifest = read_existing_manifest_for_project(root)?; let revision = read_game_creator_agent_runtime_project_revision(root)?.revision; - let canonical = root.canonicalize().map_err(|_| "项目上下文目录无法解析")?; + let canonical = root + .canonicalize() + .map_err(|error| format!("项目上下文目录无法解析:{error}"))?; let active = list_active_turns()?.into_iter().find(|turn| { Path::new(&turn.project_path).canonicalize().ok().as_ref() == Some(&canonical) }); diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index 49a6a31dd..88c96a8ad 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -91,3 +91,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - Direct 交付终态读取原先把 `session.snapshot()` 锁/状态错误折叠成 `None`,上层继续显示“尚未确认交付完成”;现沿 `code-generation` 失败出口保留终态读取正文,并在已有回合失败时同时保留原始失败与终态读取失败。 - 对话模型选择器读取 native 配置原先把 IPC/文件异常 `.catch(() => null)` 后只显示“读取客户端配置失败”;现通过统一可见错误脱敏出口保留具体正文,并保留配置不可读时不猜测模型路由的安全行为。 - 策划工作区事件订阅失败仍有一处直接静默结束,导致文件刷新失联却没有提示;现把订阅 IPC 正文写入工作区错误状态,继续保留手动刷新与已有清单读取错误出口。 +- 新一轮原生扫出 app-server 进程与上下文漏损:Codex 执行器核验/OAuth 保存/回合身份的 JoinError、图片 base64 解码、Direct 上下文 canonicalize,以及进程树归属、Job、等待、终止和 Drop 清理错误原先只剩固定码;现保留 JoinError、解码器、OS/Job、等待和终止阶段正文,并为进程树 Drop 清理失败写入诊断日志。