修复 Codex app-server 鉴权错误分类

- 识别 401/403 上游鉴权失败并归类为 unauthorized

- 增加错误分类回归测试并保持敏感诊断脱敏

- 更新 UI 工作流 Runtime 鉴权失败门禁说明
This commit is contained in:
2026-08-25 13:47:35 +08:00
parent 10786c9672
commit 96c5444972
2 changed files with 62 additions and 2 deletions
@@ -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::<Vec<_>>()
.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();
@@ -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`