diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index c05065cf7..21e2666f9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -244,12 +244,46 @@ fn game_creator_codex_app_server_connection_error( } } +fn game_creator_codex_app_server_error_detail_indicates_auth_failure( + error: &serde_json::Value, +) -> bool { + let Some(error) = error.as_object() else { + return false; + }; + let detail = ["message", "additionalDetails"] + .into_iter() + .filter_map(|field| error.get(field).and_then(serde_json::Value::as_str)) + .collect::>() + .join(" ") + .to_ascii_lowercase(); + if detail.is_empty() { + return false; + } + detail.contains("invalid token") + || detail.contains("invalid_api_key") + || detail.contains("invalid api key") + || detail.contains("401 unauthorized") + || detail.contains("403 forbidden") + || detail.contains("status 401") + || detail.contains("status 403") + || detail.contains("http 401") + || detail.contains("http 403") +} + fn game_creator_codex_app_server_failed_turn_error( turn: &serde_json::Value, ) -> platform_llm::LlmError { - let Some(info) = turn + let Some(error) = turn .get("error") - .and_then(|error| error.get("codexErrorInfo")) + .filter(|error| !error.is_null()) + else { + return game_creator_codex_app_server_error_kind("other"); + }; + if game_creator_codex_app_server_error_detail_indicates_auth_failure(error) { + return game_creator_codex_app_server_error_kind("unauthorized"); + } + let Some(info) = error + .get("codexErrorInfo") .filter(|info| !info.is_null()) else { return game_creator_codex_app_server_error_kind("other"); @@ -3593,6 +3627,30 @@ mod tests { ); } + #[test] + fn codex_app_server_failed_turn_maps_upstream_auth_details_to_unauthorized() { + for detail in [ + "unexpected status 401 Unauthorized: Invalid token", + "HTTP 403 Forbidden", + "invalid_api_key", + ] { + let error = game_creator_codex_app_server_failed_turn_error(&serde_json::json!({ + "status": "failed", + "error": { + "message": detail, + "additionalDetails": "private upstream diagnostics", + "codexErrorInfo": "other" + } + })); + assert_eq!( + error, + platform_llm::LlmError::InvalidRequest( + "codex-app-server-error:unauthorized".to_string() + ) + ); + } + } + #[test] fn codex_app_server_rejects_non_responses_key_mapping() { let mut llm = test_llm(); diff --git a/docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md b/docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md index e81e28841..ed0ad5971 100644 --- a/docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md +++ b/docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md @@ -55,6 +55,8 @@ ui-workflow.completed `recognize` 现在直接复用 UI Editor 的 provider-backed `recognize_ui_impl`、`merge_ui_impl` 与 `bind_components_impl`:先对页面设计图执行多模态结构识别,再落盘合并后的唯一页面树,最后按 5 项一批绑定已登记图片/图标,并向模型提供 State 内已验证字体的 ID、family、face、weight 与 style。由 Agent Runtime 调用时,这三个阶段携带当前 `agent_id/run_id`,统一走活动 Provider 的 mode、请求快照、重试和恢复链路,不再从工作流偷偷创建另一套传统 HTTP client。Codex app-server 会把输入图片暂存到该连接的隔离工作区 `input-images/`,通过原生 `localImage` 输入发送;文本提示只保留图片占位符,避免把 base64 复制进提示词或 JSON-RPC。所有 LLM 工具参数仍沿用 UI Editor 的严格 schema、节点/深度/素材和字体白名单及有界输入校验。Provider 未配置、请求失败、工具调用缺失、结果不匹配、未知字体引用、绑定没有可渲染组件或仍有 `NeedReview/Blocked` 时,完成阶段不会推进;已落盘的中间阶段仍通过 manifest invalidation 更新客户端,不再使用 deterministic seed 冒充语义处理通过。 +真实 Provider 鉴权失败时,Codex app-server 可能只返回 `codexErrorInfo=other`,而把上游 `401/403` 放在错误正文中。Runtime 必须从受控错误字段识别为 `codex-app-server-error:unauthorized`(公共摘要为 `codex-app-server-unauthorized`),只向公共运行记录暴露错误类别和指纹,不记录 Token 或上游原文。此错误不能伪造为 UI 工作流阶段完成;修复凭据后应从原有 run 的恢复边界重新执行。 + ## 画布跳转与客户端更新 点击画布中的 `ui-prototype` 时,工作台调用 `ensure_ui_design_resource_for_prototype`: