diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 53eff6757..ed89d3a1d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -2907,7 +2907,12 @@ async fn recover_direct_taonier_spritesheet_read_only_at( access.validate_frozen_session()?; let status = response.status(); if !status.is_success() { - return Err(format!("读取陶泥儿画布资源失败:HTTP {}", status.as_u16())); + let body = response.text().await.unwrap_or_default(); + return Err(crate::assets::external_asset_http_failure( + "读取陶泥儿画布资源", + status, + &body, + )); } let payload = response .json::() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index 745cbd6c7..00d800c36 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -933,8 +933,8 @@ pub(crate) fn classify_external_generation_initial_response( )) } _ => Err(format!( - "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 平台图片生成返回未识别的成功状态 HTTP {}", - status.as_u16() + "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} {}", + format_external_http_error("平台图片生成", status, &payload.to_string()) )), } } @@ -1361,9 +1361,11 @@ pub(crate) async fn submit_external_generation_request( ) })?; if response.status().is_server_error() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); return Err(format!( - "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 请求平台图片生成后收到 HTTP {},服务端是否已产生副作用未知", - response.status().as_u16() + "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 请求平台图片生成后收到 {},服务端是否已产生副作用未知", + format_external_http_error("平台图片生成", status, &body) )); } Ok(response) @@ -1394,8 +1396,8 @@ async fn resume_prepared_external_generation_at( let body = response.text().await.unwrap_or_default(); crate::platform_maintenance::watch_platform_response(status.as_u16(), &body); return Err(format!( - "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} External Editor prepared 恢复提交返回 HTTP {};原生成账本已保留", - status.as_u16() + "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} External Editor prepared 恢复提交返回 {};原生成账本已保留", + format_external_http_error("External Editor prepared 恢复提交", status, &body) )); } let submission_payload_result = response @@ -2137,9 +2139,12 @@ async fn upload_manifest_asset_remote_reference_at( .await .map_err(|error| format!("上传当前账号参考图失败:{error}"))?; if upload_response.status().as_u16() != ticket.success_action_status { - return Err(format!( - "上传当前账号参考图失败:HTTP {}", - upload_response.status().as_u16() + let status = upload_response.status(); + let body = upload_response.text().await.unwrap_or_default(); + return Err(crate::assets::external_asset_http_failure( + "上传当前账号参考图", + status, + &body, )); } access.validate_frozen_session()?; diff --git a/apps/ai-game-creator-shell/src-tauri/src/assets.rs b/apps/ai-game-creator-shell/src-tauri/src/assets.rs index f7881313c..9da8c8460 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/assets.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/assets.rs @@ -11,6 +11,60 @@ const PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_STORAGE_PREPARATION_FAILURE: &str = const PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_PERSISTENCE_FAILURE: &str = "private-external-editor-credential-persistence-failed"; +/// Extract a bounded, redacted upstream error without treating a status code as the whole reason. +fn external_asset_http_detail(body: &str) -> Option { + let parsed = serde_json::from_str::(body).ok(); + let error = parsed + .as_ref() + .and_then(|value| value.get("error")) + .or_else(|| parsed.as_ref()); + let detail = ["message", "detail", "code", "additionalDetails"] + .into_iter() + .filter_map(|field| { + error + .and_then(|value| value.get(field)) + .and_then(serde_json::Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string) + }) + .collect::>() + .join(";"); + let detail = (!detail.is_empty()).then_some(detail).or_else(|| { + let safe = crate::agent::redact_agent_runtime_error( + Path::new("__agc_no_project_root__"), + body.trim(), + 600, + ); + (!safe.trim().is_empty() && !matches!(safe.trim(), "{}" | "null" | "\"\"")).then_some(safe) + })?; + let detail = crate::agent::redact_agent_runtime_error( + Path::new("__agc_no_project_root__"), + &detail, + 600, + ); + (!detail.trim().is_empty()).then_some(detail) +} + +pub(crate) fn external_asset_http_failure( + action: &str, + status: reqwest::StatusCode, + body: &str, +) -> String { + let detail = external_asset_http_detail(body) + .map(|detail| format!(":{detail}")) + .unwrap_or_default(); + match status { + reqwest::StatusCode::UNAUTHORIZED => { + format!("authentication-required: {action}失败(HTTP 401),请重新登录后重试{detail}") + } + reqwest::StatusCode::FORBIDDEN => { + format!("permission-denied: {action}被服务器拒绝(HTTP 403){detail}") + } + _ => format!("{action}失败:HTTP {}{detail}", status.as_u16()), + } +} + /// A task-scoped External Editor credential used only by the direct Codex /// runtime. It never changes the GUI account session and deliberately keeps /// the key private: callers may use it to make authenticated requests but may @@ -406,15 +460,11 @@ async fn create_private_external_editor_api_credentials_from_platform_session( if !status.is_success() { let body = response.text().await.unwrap_or_default(); crate::platform_maintenance::watch_platform_response(status.as_u16(), &body); - return Err(match status { - reqwest::StatusCode::UNAUTHORIZED => { - "authentication-required: 陶泥儿登录已失效,无法创建本机开发者 Key".to_string() - } - reqwest::StatusCode::FORBIDDEN => { - "permission-denied: 当前陶泥儿账号无权创建本机开发者 Key".to_string() - } - _ => format!("创建本机陶泥儿开发者 Key 失败:HTTP {}", status.as_u16()), - }); + return Err(external_asset_http_failure( + "创建本机陶泥儿开发者 Key", + status, + &body, + )); } let payload = response .json::() @@ -907,9 +957,10 @@ pub(crate) async fn sync_canvas_project_assets_at( if !project_status.is_success() { let body = project_response.text().await.unwrap_or_default(); crate::platform_maintenance::watch_platform_response(project_status.as_u16(), &body); - return Err(format!( - "读取画板项目失败:HTTP {}", - project_status.as_u16() + return Err(external_asset_http_failure( + "读取画板项目", + project_status, + &body, )); } let project_payload = project_response @@ -1364,7 +1415,8 @@ where return Err("画板资产下载地址发生重定向,已拒绝继续请求".to_string()); } if !status.is_success() { - return Err(format!("下载画板资产失败:HTTP {}", status.as_u16())); + let body = response.text().await.unwrap_or_default(); + return Err(external_asset_http_failure("下载画板资产", status, &body)); } if response .content_length() @@ -1436,7 +1488,7 @@ pub(crate) async fn resolve_external_asset_signed_url( if !status.is_success() { let body = response.text().await.unwrap_or_default(); crate::platform_maintenance::watch_platform_response(status.as_u16(), &body); - return Err(format!("换签画板资产失败:HTTP {}", status.as_u16())); + return Err(external_asset_http_failure("换签画板资产", status, &body)); } let payload = response .json::() @@ -2257,6 +2309,19 @@ mod tests { use shared_contracts::game_creation_app::GameCreationAppAssetCategory; use std::io::{Read, Write}; + #[test] + fn external_asset_http_failure_keeps_status_and_redacted_upstream_detail() { + let detail = external_asset_http_failure( + "读取画板项目", + reqwest::StatusCode::UNAUTHORIZED, + r#"{"error":{"code":"TOKEN_EXPIRED","message":"access token expired;authorization=Bearer secret"}}"#, + ); + assert!(detail.contains("HTTP 401"), "{detail}"); + assert!(detail.contains("TOKEN_EXPIRED"), "{detail}"); + assert!(detail.contains("access token expired"), "{detail}"); + assert!(!detail.contains("Bearer secret"), "{detail}"); + } + #[test] fn design_artifact_registration_reports_only_real_manifest_changes() { let temporary = tempfile::tempdir().expect("tempdir"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index ea9d974da..32f57ecfb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -3706,9 +3706,12 @@ async fn download_ui_editor_remote_asset(url: &str, max_bytes: u64) -> Result String { } fn response_error(status: StatusCode, body: &str) -> String { - if status == StatusCode::UNAUTHORIZED { - return "authentication-required: 陶泥儿登录态已过期,请重新登录后重试".to_string(); - } - if status == StatusCode::FORBIDDEN { - return "permission-denied: 当前账号没有提交错误报告的权限".to_string(); - } let detail = serde_json::from_str::(body) .ok() .and_then(|value| { let error = value.get("error").unwrap_or(&value); - error - .get("message") - .and_then(Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToString::to_string) - .or_else(|| { - error - .get("code") - .and_then(Value::as_str) - .map(str::to_string) - }) - .or_else(|| serde_json::to_string(error).ok()) + Some( + ["message", "detail", "code", "additionalDetails"] + .into_iter() + .filter_map(|field| { + error + .get(field) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string) + }) + .collect::>() + .join(";"), + ) }) + .filter(|value| !value.is_empty()) .or_else(|| { let detail = crate::agent::redact_agent_runtime_error( Path::new("__agc_no_project_root__"), body.trim(), 600, ); - (!detail.trim().is_empty()).then_some(detail) + (!detail.trim().is_empty() && !matches!(detail.trim(), "{}" | "null" | "\"\"")) + .then_some(detail) }) - .unwrap_or_else(|| format!("HTTP {}", status.as_u16())); - format!("错误报告提交失败:{detail}") + .map(|detail| { + crate::agent::redact_agent_runtime_error( + Path::new("__agc_no_project_root__"), + &detail, + 600, + ) + }) + .filter(|value| !value.trim().is_empty()); + if status == StatusCode::UNAUTHORIZED { + return match detail { + Some(detail) => { + format!("authentication-required: 陶泥儿登录态已过期,请重新登录后重试:{detail}") + } + None => "authentication-required: 陶泥儿登录态已过期,请重新登录后重试".to_string(), + }; + } + if status == StatusCode::FORBIDDEN { + return format!( + "permission-denied: 当前账号没有提交错误报告的权限{}", + detail + .map(|detail| format!(":{detail}")) + .unwrap_or_default() + ); + } + format!( + "错误报告提交失败:HTTP {}{}", + status.as_u16(), + detail + .map(|detail| format!(":{detail}")) + .unwrap_or_default() + ) } fn response_data(body: &str) -> Result { @@ -234,4 +260,16 @@ mod tests { assert_eq!(value["batchId"], "batch-1"); assert!(!value.to_string().contains("token")); } + + #[test] + fn response_error_keeps_status_and_redacted_server_detail() { + let detail = response_error( + StatusCode::TOO_MANY_REQUESTS, + r#"{"error":{"code":"RATE_LIMITED","message":"too many requests;token=secret"}}"#, + ); + assert!(detail.contains("HTTP 429"), "{detail}"); + assert!(detail.contains("RATE_LIMITED"), "{detail}"); + assert!(detail.contains("too many requests"), "{detail}"); + assert!(!detail.contains("token=secret"), "{detail}"); + } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/platform_asset_upload.rs b/apps/ai-game-creator-shell/src-tauri/src/platform_asset_upload.rs index 90406c6d6..fc7e3988d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/platform_asset_upload.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/platform_asset_upload.rs @@ -376,10 +376,13 @@ async fn upload_object( .await .map_err(|error| format!("上传素材失败:无法访问素材存储:{error};请检查网络后重试"))?; validate_session(snapshot)?; - if response.status().as_u16() != ticket.success_action_status { - return Err(format!( - "上传素材到对象存储失败(HTTP {}),请重试", - response.status().as_u16() + let status = response.status(); + if status.as_u16() != ticket.success_action_status { + let body = response.text().await.unwrap_or_default(); + return Err(crate::assets::external_asset_http_failure( + "上传素材到对象存储", + status, + &body, )); } Ok(()) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs index 6e3c05987..e8b8e4ff8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs @@ -1768,32 +1768,31 @@ async fn request_resource_edit_upload_ticket( .await .map_err(|error| format!("result-unknown: 创建源资源上传凭证请求失败:{error}"))?; let mut post_response_session_error = access.validate_frozen_session().err(); - if response.status() == reqwest::StatusCode::UNAUTHORIZED { - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(editor_api_authentication_error()); + let status = response.status(); + if status == reqwest::StatusCode::UNAUTHORIZED { + let base = post_response_session_error + .clone() + .unwrap_or_else(editor_api_authentication_error); + return Err(append_editor_api_reason(base, response).await); } - if response.status() == reqwest::StatusCode::FORBIDDEN { - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(editor_api_authorization_error()); + if status == reqwest::StatusCode::FORBIDDEN { + let base = post_response_session_error + .clone() + .unwrap_or_else(editor_api_authorization_error); + return Err(append_editor_api_reason(base, response).await); } - if !response.status().is_success() { - if response.status().is_server_error() { - return Err(format!( + if !status.is_success() { + let base = if status.is_server_error() { + format!( "result-unknown: 创建源资源上传凭证返回 HTTP {}", - response.status().as_u16() - )); - } - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(format!( - "创建源资源上传凭证失败:HTTP {}", - response.status().as_u16() - )); + status.as_u16() + ) + } else if let Some(error) = post_response_session_error.as_ref() { + error.clone() + } else { + format!("创建源资源上传凭证失败:HTTP {}", status.as_u16()) + }; + return Err(append_editor_api_reason(base, response).await); } let payload = response .json::() @@ -1901,19 +1900,16 @@ async fn upload_resource_edit_source( .map_err(|error| format!("result-unknown: 上传源资源请求失败:{error}"))?; let post_response_session_error = access.validate_frozen_session().err(); if response.status().as_u16() != ticket.success_action_status { + let status = response.status(); if response.status().is_server_error() { - return Err(format!( - "result-unknown: 上传源资源返回 HTTP {}", - response.status().as_u16() - )); + let base = format!("result-unknown: 上传源资源返回 HTTP {}", status.as_u16()); + return Err(append_editor_api_reason(base, response).await); } if let Some(error) = post_response_session_error { - return Err(error); + return Err(append_editor_api_reason(error, response).await); } - return Err(format!( - "上传源资源失败:HTTP {}", - response.status().as_u16() - )); + let base = format!("上传源资源失败:HTTP {}", status.as_u16()); + return Err(append_editor_api_reason(base, response).await); } Ok(ResourceEditRemoteStage { value: (), @@ -1955,32 +1951,31 @@ async fn confirm_resource_edit_source( .await .map_err(|error| format!("result-unknown: 确认源资源上传请求失败:{error}"))?; let mut post_response_session_error = access.validate_frozen_session().err(); - if response.status() == reqwest::StatusCode::UNAUTHORIZED { - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(editor_api_authentication_error()); + let status = response.status(); + if status == reqwest::StatusCode::UNAUTHORIZED { + let base = post_response_session_error + .clone() + .unwrap_or_else(editor_api_authentication_error); + return Err(append_editor_api_reason(base, response).await); } - if response.status() == reqwest::StatusCode::FORBIDDEN { - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(editor_api_authorization_error()); + if status == reqwest::StatusCode::FORBIDDEN { + let base = post_response_session_error + .clone() + .unwrap_or_else(editor_api_authorization_error); + return Err(append_editor_api_reason(base, response).await); } - if !response.status().is_success() { - if response.status().is_server_error() { - return Err(format!( + if !status.is_success() { + let base = if status.is_server_error() { + format!( "result-unknown: 确认源资源上传返回 HTTP {}", - response.status().as_u16() - )); - } - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(format!( - "确认源资源上传失败:HTTP {}", - response.status().as_u16() - )); + status.as_u16() + ) + } else if let Some(error) = post_response_session_error.as_ref() { + error.clone() + } else { + format!("确认源资源上传失败:HTTP {}", status.as_u16()) + }; + return Err(append_editor_api_reason(base, response).await); } let payload = response .json::() @@ -2067,32 +2062,31 @@ async fn register_resource_edit_source_image( .await .map_err(|error| format!("result-unknown: 登记源图片项目资源请求失败:{error}"))?; let mut post_response_session_error = access.validate_frozen_session().err(); - if response.status() == reqwest::StatusCode::UNAUTHORIZED { - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(editor_api_authentication_error()); + let status = response.status(); + if status == reqwest::StatusCode::UNAUTHORIZED { + let base = post_response_session_error + .clone() + .unwrap_or_else(editor_api_authentication_error); + return Err(append_editor_api_reason(base, response).await); } - if response.status() == reqwest::StatusCode::FORBIDDEN { - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(editor_api_authorization_error()); + if status == reqwest::StatusCode::FORBIDDEN { + let base = post_response_session_error + .clone() + .unwrap_or_else(editor_api_authorization_error); + return Err(append_editor_api_reason(base, response).await); } - if !response.status().is_success() { - if response.status().is_server_error() { - return Err(format!( + if !status.is_success() { + let base = if status.is_server_error() { + format!( "result-unknown: 登记源图片项目资源返回 HTTP {}", - response.status().as_u16() - )); - } - if let Some(error) = post_response_session_error { - return Err(error); - } - return Err(format!( - "登记源图片项目资源失败:HTTP {}", - response.status().as_u16() - )); + status.as_u16() + ) + } else if let Some(error) = post_response_session_error.as_ref() { + error.clone() + } else { + format!("登记源图片项目资源失败:HTTP {}", status.as_u16()) + }; + return Err(append_editor_api_reason(base, response).await); } let payload = response .json::() @@ -2667,6 +2661,13 @@ async fn editor_api_rejection_reason(response: reqwest::Response) -> Option String { + match editor_api_rejection_reason(response).await { + Some(reason) => format!("{base}:{reason}"), + None => base, + } +} + /// 追加到用户可见原因里的外部字段上限:正文已经按 64KiB 收口,但单个字段仍可能把 /// 「不让整段正文进 UI」的初衷抵消掉,所以每个追加项单独截断。 const MAX_EDITOR_ERROR_DETAIL_CHARS: usize = 80; @@ -2788,16 +2789,16 @@ async fn submit_resource_edit_remote( let mut post_response_session_error = access.validate_frozen_session().err(); let status = response.status(); if status == reqwest::StatusCode::UNAUTHORIZED { - if let Some(error) = post_response_session_error.as_ref() { - return Err(error.clone()); - } - return Err(editor_api_authentication_error()); + let base = post_response_session_error + .clone() + .unwrap_or_else(editor_api_authentication_error); + return Err(append_editor_api_reason(base, response).await); } if status == reqwest::StatusCode::FORBIDDEN { - if let Some(error) = post_response_session_error.as_ref() { - return Err(error.clone()); - } - return Err(editor_api_authorization_error()); + let base = post_response_session_error + .clone() + .unwrap_or_else(editor_api_authorization_error); + return Err(append_editor_api_reason(base, response).await); } if status == reqwest::StatusCode::BAD_REQUEST { ledger.terminal_failure_code = Some("remote-request-bad-request".to_string()); @@ -2934,10 +2935,13 @@ async fn wait_for_resource_edit_remote( continue; } if !response.status().is_success() { - return Err(ResourceEditError::Other(format!( + let base = format!( "result-unknown: 查询资源编辑任务返回 HTTP {}", response.status().as_u16() - ))); + ); + return Err(ResourceEditError::Other( + append_editor_api_reason(base, response).await, + )); } let payload = response .json::() diff --git a/apps/ai-game-creator-shell/src-tauri/src/project_snapshot/transport.rs b/apps/ai-game-creator-shell/src-tauri/src/project_snapshot/transport.rs index 6ff4c226a..4b9a2901f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project_snapshot/transport.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project_snapshot/transport.rs @@ -323,7 +323,13 @@ async fn project_snapshot_response_error( let detail = response .text() .await - .map(|body| body.chars().take(200).collect::()) + .map(|body| { + crate::agent::redact_agent_runtime_error( + std::path::Path::new("__agc_no_project_root__"), + &body, + 200, + ) + }) .unwrap_or_default(); let detail = detail.trim().to_string(); if detail.is_empty() { diff --git a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs index 65265250e..0c1e9a7af 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs @@ -572,7 +572,13 @@ async fn fetch_limited_bytes( .await .map_err(|error| format!("请求模板库失败:{error}"))?; if !response.status().is_success() { - return Err(format!("模板库返回 HTTP {}", response.status().as_u16())); + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(crate::assets::external_asset_http_failure( + "模板库请求", + status, + &body, + )); } if response .content_length() diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow/extract.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow/extract.rs index 9477e2a37..351df5525 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow/extract.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow/extract.rs @@ -118,11 +118,17 @@ async fn raw_extract_inner( format!("图片分离请求失败:{error}") })?; if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); app_log!( "ui_separation.error stage=image_edit reason=http_status status={}", - response.status() + status ); - return Err(format!("图片分离请求失败(HTTP {})", response.status())); + return Err(crate::assets::external_asset_http_failure( + "图片分离请求", + status, + &body, + )); } let payload = response.json::().await.map_err(|error| { app_log!("ui_separation.error stage=image_edit reason=parse_response error={error}"); diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index 73bd925cc..63b172a29 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -131,6 +131,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - 外部 MCP loopback 的 401/429/桥上下文缺失必须返回带正文的 JSON 错误体(同时保留 HTTP 状态);不能让 Claude Code 侧只看到空 body 的状态码。 - 认证 5xx、账户/素材上传/发行发布的 401/403 保留服务端 `message`/`code` 后再做精确脱敏;空 `{}`、`null` 等无信息响应不算错误正文。 - 旧版已落盘的通用“执行通道中断”文本无法恢复历史原因,读取时明确追加“宿主未提供具体错误正文”;新事件只在确实没有 detail 时使用同样标记。 +- 画板/模板/图片分离/参考图/External Editor/资源编辑/项目快照/错误报告等外围 HTTP 入口也遵循同一出口:读取有限响应正文,保留状态码、服务端 message/code 和操作上下文,凭据、URL、路径按统一规则脱敏;不能因为不是 Direct 回合主链就回到“HTTP N”兜底。 ## 后果与边界 diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index 4fbdfa09b..da9e89b3f 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -52,3 +52,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - 本轮继续扫出三类未收敛口:外部 MCP HTTP 的 401/429/桥上下文缺失原先只有空状态码响应,现改为保留 HTTP 状态、稳定错误码和中文正文;认证 5xx 结构化载荷、账户/素材上传/发行发布的 401/403 现保留服务端 message/code(仍精确脱敏)。 - 前端历史错误文本此前会把含有敏感字段的整条 Direct 诊断直接丢弃,现改为只替换凭据、链接和路径;旧版已经落盘且无法恢复正文的“执行通道中断”条目会明确追加“宿主未提供具体错误正文”,不再伪装成当前仍在吞错。 - 新增/通过:外部 MCP HTTP 401/429 定向 2 项、前端 Direct/Runtime 错误正文 25 项、旧历史通用中断脱敏回归 1 项;认证 Rust 定向 19 项、素材上传响应错误定向 1 项通过。`appSurface` 全套仍有环境/fixture 相关失败,不能据此宣称全绿。 +- 继续扫描又发现画板项目/资产下载、模板库、图片分离、参考图上传、External Editor 生成恢复、资源编辑上传/轮询、项目快照和错误报告提交仍有“HTTP 只有编号”或未脱敏正文路径;已统一补上状态码 + 有界正文 + 精确脱敏。资源编辑拒绝原因继续复用既有 `details.message` 解析,避免丢掉平台具体原因。