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 4476da562..db3d8a0a1 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 @@ -343,6 +343,26 @@ fn game_creator_codex_app_server_error_kind_with_machine_detail( .collect::>(); if !keys.is_empty() { fields.push(format!("fields={}", keys.join(","))); + // `codexErrorInfo` is often the only field returned by app-server for a + // failed turn. Keeping just its key (the old behaviour) turns a real + // HTTP/connection failure into `other detail=fields=codexErrorInfo`. + // Preserve the bounded structured value as diagnostic text; the same + // redaction boundary below removes credentials, URLs and paths while + // retaining status codes and actionable provider fields. + let structured_details = keys + .iter() + .filter_map(|key| { + let value = object.get(key)?; + let serialized = serde_json::to_string(value).ok()?; + let safe = crate::agent::redact_agent_runtime_error( + std::path::Path::new("__agc_no_project_root__"), + &serialized, + 360, + ); + (!safe.trim().is_empty()).then(|| format!("{key}={safe}")) + }) + .collect::>(); + fields.extend(structured_details); } let detail = ["message", "additionalDetails"] .into_iter() @@ -7105,6 +7125,42 @@ mod tests { assert!(!visible.contains("victim")); } + #[test] + fn codex_app_server_failed_turn_keeps_nested_error_info_details() { + let failed_turn = serde_json::json!({ + "status": "failed", + "error": { + "codexErrorInfo": { + "httpConnectionFailed": { + "httpStatusCode": 400, + "message": "provider rejected the request" + }, + "responseStreamConnectionFailed": { + "retryable": false + } + } + } + }); + let visible = game_creator_codex_app_server_failed_turn_error(&failed_turn).to_string(); + assert!(visible.contains("httpStatusCode"), "{visible}"); + assert!(visible.contains("400"), "{visible}"); + assert!( + visible.contains("provider rejected the request"), + "{visible}" + ); + } + + #[test] + fn codex_app_server_failed_turn_keeps_machine_only_error_info_value() { + let failed_turn = serde_json::json!({ + "status": "failed", + "error": { "codexErrorInfo": "other" } + }); + let visible = game_creator_codex_app_server_failed_turn_error(&failed_turn).to_string(); + assert!(visible.contains("fields=codexErrorInfo"), "{visible}"); + assert!(visible.contains("codexErrorInfo=\"other\""), "{visible}"); + } + #[test] fn codex_app_server_failed_turn_maps_stable_categories_and_http_status() { for (info, expected_prefix) in [ diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index e91109988..1f2e79f57 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -106,6 +106,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - `turn.completed.failure` 中的 `detail` / `diagnostic` 不再一律隐藏。前端按 typed 变体展示安全正文,保留 HTTP 状态、错误码、IPC/stdio、操作系统错误(例如 `EPIPE`、`ENOMEM`、`ENOENT`)和上游可行动说明。 - 脱敏只替换敏感值、完整 URL、绝对路径和私钥内容;字段名(例如 `Authorization`、`token`、`api_key`)保留,便于判断错误类别。原始凭据值不进入用户文本、应用日志或项目诊断。 - 上游 app-server 映射保留 `message` / `additionalDetails` 的脱敏摘要;因此 401/403/408/429/413/5xx 等状态不会因为分类为 transport 或 native 而丢失正文。 +- app-server 失败对象只有 `codexErrorInfo` 嵌套字段时,也保留其有界 JSON 值(例如 `httpStatusCode`、连接阶段和 `retryable`);不能只留下 `fields=codexErrorInfo`,否则真实 400/401 会再次退化成分类兜底。 - Claude Code(cc)侧车的非零退出、RPC `error`、stdout JSON/UTF-8 解析失败、stderr 和静默超时也走同一映射;侧车 stderr 只在有界收口窗口内读取并进入同一脱敏 detail,不再只写裸 `eprintln!`。 - Codex CLI / Claude sidecar 的 JSONL/JSON 解析失败、失败终态、缺失终态、超时和空回执都保留解析错误、失败事件正文与有界安全片段;`LlmError` 没有 detail 字段的超时/空回执由 Direct `ModelCallFailed.detail` 补回,不再把这些协议事实统一压成单一类别。 - 账户、模型目录、External Editor/资源编辑、发布、素材上传、错误报告与客户端受控工具桥共用同一原则:网络错误保留底层因链,JSON/协议解析保留 serde 原因,HTTP 错误保留状态码与安全正文;只有响应完全没有正文时才说明“未提供 error/code/message”,不得退成“无法连接/格式无效/服务器未返回错误信息”。 diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index b423a4beb..76f760f71 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -13,6 +13,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - `turn.completed.failure` 的前端展示:按 typed 变体显示分类、状态码和脱敏后的 `detail` / `diagnostic`。 - Rust 侧错误文本脱敏:只替换敏感值、URL、绝对路径和私钥内容,保留 HTTP 状态、错误码、字段名和可行动描述。 +- app-server `codexErrorInfo` 只有嵌套机器字段时,保留经有界脱敏的结构化值;`fields=codexErrorInfo` 不能成为唯一正文。 - Transport / Stream / IPC / host process / memory exhaustion 的回归测试与错误事件证据。 ## 不做 @@ -34,7 +35,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - `npx vitest run tests/directTurnFailure.test.ts`:15 passed。 - `npx tsc --noEmit -p apps/ai-game-creator-shell/tsconfig.json`:通过。 -- Rust app-server 上游映射定向测试:6 passed;应用日志脱敏测试与启动诊断脱敏测试均通过。 +- Rust app-server 上游映射定向测试:8 passed;覆盖嵌套 `codexErrorInfo` 与只有机器字段的错误值;应用日志脱敏测试与启动诊断脱敏测试均通过。 - CC 错误分类回归:Claude sidecar 状态码/超时映射与非支付 409 保留上游状态的定向测试通过;侧车失败统一补充有界 stderr detail。 - 真实 AGC dev smoke:客户端使用 `3080`、后端 `8084`、数据库 `3101`、后台 `3103` 启动;真实 CC 回合复现 `Reached maximum number of turns (8)` 被旧代码错误记为 `transport-closed`,修复后 Tauri 已热重编译重启。修复后的真实 Provider 回放未再次发送,避免无必要的付费请求。 -- 真实 Provider、真实 IPC 断链和真实内存压力尚未在本轮执行。 +- 真实 AGC 项目诊断发现真实请求 HTTP 400 曾被摘要为 `codex-app-server-error:other detail=fields=codexErrorInfo`,另有 401 仅显示 `codex-app-server-error:unauthorized`;该漏损已在 app-server 投影边界修复。真实 Provider、真实 IPC 断链和真实内存压力仍未在本轮主动制造。