From 00af3d9c88e0d40b5f27b34578d88b3a898afcc0 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:13:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99=20CLI=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF=E6=AD=A3=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 Codex CLI 与 Claude sidecar 失败终态和无效 JSON 细节 添加脱敏协议错误回归 --- .../src-tauri/src/agent/claude_code_cli.rs | 13 +++-- .../src-tauri/src/agent/codex_cli.rs | 50 +++++++++++++++++-- ...GC命令错误结构化与错误报告口径-2026-10-01.md | 1 + ...实施计划】AGC错误具体文本展示-2026-10-04.md | 1 + 4 files changed, 56 insertions(+), 9 deletions(-) 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 ba60c1c92..1f5b65e88 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 @@ -290,10 +290,15 @@ async fn run_sidecar_turn( if line.trim().is_empty() { continue; } - let value: serde_json::Value = serde_json::from_str(&line).map_err(|_| { - SidecarTurnFailure::message( - "Claude Agent SDK sidecar 输出不是有效 JSON".to_string(), - ) + let value: serde_json::Value = serde_json::from_str(&line).map_err(|error| { + let detail = crate::agent::redact_agent_runtime_error( + Path::new("__agc_no_project_root__"), + &line, + 480, + ); + SidecarTurnFailure::message(format!( + "Claude Agent SDK sidecar 输出不是有效 JSON:{error};原文={detail}" + )) })?; match value.get("type").and_then(serde_json::Value::as_str) { Some("event") => { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs index 62fe0f7bc..bbe154ba8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs @@ -543,9 +543,14 @@ fn parse_game_creator_codex_cli_response( if line.trim().is_empty() { continue; } - let event = serde_json::from_str::(line).map_err(|_| { + let event = serde_json::from_str::(line).map_err(|error| { + let detail = crate::agent::redact_agent_runtime_error( + Path::new("__agc_no_project_root__"), + line, + 480, + ); platform_llm::LlmError::Deserialize(format!( - "Codex CLI Agent JSONL 第 {} 行无效", + "Codex CLI Agent JSONL 第 {} 行无效:{error};原文={detail}", index.saturating_add(1) )) })?; @@ -587,9 +592,21 @@ fn parse_game_creator_codex_cli_response( } } Some("turn.failed") => { - return Err(platform_llm::LlmError::Transport( - "Codex CLI Agent 返回失败终态".to_string(), - )); + let detail = event + .get("error") + .or_else(|| event.get("message")) + .map(serde_json::Value::to_string) + .unwrap_or_default(); + let detail = crate::agent::redact_agent_runtime_error( + Path::new("__agc_no_project_root__"), + detail.trim_matches('"'), + 480, + ); + return Err(platform_llm::LlmError::Transport(if detail.is_empty() { + "Codex CLI Agent 返回失败终态".to_string() + } else { + format!("Codex CLI Agent 返回失败终态:{detail}") + })); } _ => {} } @@ -1219,6 +1236,29 @@ mod tests { ); } + #[test] + fn codex_cli_mode_preserves_failed_terminal_and_invalid_json_details() { + let request = LlmRunRequest::single_turn("系统", "任务"); + let failed = parse_game_creator_codex_cli_response( + b"{\"type\":\"turn.failed\",\"error\":{\"message\":\"HTTP 502 upstream overloaded\",\"api_key\":\"fixture-secret\"}}\n", + &request, + ) + .expect_err("failed terminal must be returned as an error"); + let failed_text = failed.to_string(); + assert!(failed_text.contains("HTTP 502 upstream overloaded")); + assert!(!failed_text.contains("fixture-secret")); + + let invalid = parse_game_creator_codex_cli_response( + b"{not-json Authorization: Bearer fixture-secret}\n", + &request, + ) + .expect_err("invalid JSONL must preserve a bounded safe line detail"); + let invalid_text = invalid.to_string(); + assert!(invalid_text.contains("JSONL 第 1 行无效")); + assert!(invalid_text.contains("原文=")); + assert!(!invalid_text.contains("fixture-secret")); + } + #[cfg(unix)] #[tokio::test] async fn codex_cli_mode_builds_ephemeral_json_exec_with_prompt_on_stdin() { diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index 0c8b2a1e7..1390fd2d7 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -107,6 +107,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - 脱敏只替换敏感值、完整 URL、绝对路径和私钥内容;字段名(例如 `Authorization`、`token`、`api_key`)保留,便于判断错误类别。原始凭据值不进入用户文本、应用日志或项目诊断。 - 上游 app-server 映射保留 `message` / `additionalDetails` 的脱敏摘要;因此 401/403/408/429/413/5xx 等状态不会因为分类为 transport 或 native 而丢失正文。 - Claude Code(cc)侧车的非零退出、RPC `error`、stdout JSON/UTF-8 解析失败、stderr 和静默超时也走同一映射;侧车 stderr 只在有界收口窗口内读取并进入同一脱敏 detail,不再只写裸 `eprintln!`。 +- Codex CLI / Claude sidecar 的 JSONL/JSON 解析失败和失败终态保留解析错误、失败事件正文与有界安全片段,不再把无效 JSON、`turn.failed` 或缺失终态统一压成单一类别。 - HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。 - `hostDropped` 携带可选的脱敏 `detail`:panic hook 能取得的负载和位置随原回合占用进入失败事件;普通 Drop 仅记录“退出时未写终态、未观察到 panic”,不推断为网络中断。旧版本无 `detail` 的事件继续可读,界面明确标注旧事件未记录原因。 - DirectProject 控制器的取消待发、终止、附件上传、项目历史读取和发送前置异常也复用同一份前端精确脱敏出口;这些非回合错误不得直接把 `Error.message` 原文写入状态栏、composer 或历史错误行。 diff --git a/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md index ead45363b..76da95c54 100644 --- a/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md @@ -17,6 +17,7 @@ Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md` 8. 项目资源编辑/上传/预览/恢复队列/生成结果回读的 UI 错误统一使用 `normalizeDiagnosticText`,不再直出 `Error.message`。 9. 应用壳、运行配置、认证状态、插件启动、运行预览和策划会话的非结构化错误统一使用 `visibleClientErrorMessage`。 10. 资产导入、邀请码、策划工作区、发布封面/截图和素材命令的 UI 错误沿用同一共享出口。 +11. Codex CLI / Claude sidecar 的解析失败与失败终态保留有界脱敏正文,避免协议错误只剩类别码。 ## 实现顺序