From c20e28bed0f9dd7f7c6eb1691a7fc9bb5cd09a41 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Thu, 10 Sep 2026 19:01:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20AGC=20=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E7=BC=96=E8=BE=91=204xx=20=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0?= =?UTF-8?q?=E5=8F=96=E5=80=BC=EF=BC=9A=E4=BC=98=E5=85=88=20details.message?= =?UTF-8?q?=20=E5=B9=B6=E9=99=84=20provider=20=E4=B8=8E=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `editor_api_rejection_reason` 取值顺序改为 `error.details.message` → `details.message` → `error.message` → `error` 文本 → 顶层 `message` - 平台把所有 4xx 兜底成「请求参数不合法」,真实原因写在 `details.message`,原实现只读 `error.message` 导致用户永远看不到原因 - `details` 有值时附带 `provider`,并透出 `assetKind` / `mediaType`,非 BAD_REQUEST 的稳定错误码照旧透出 - 抽出同步函数 `editor_api_rejection_reason_from_body` 并补定向用例,钉住取值顺序、截断与兜底行为 --- .../src-tauri/src/project/resource_editor.rs | 74 +++++++++++++------ 1 file changed, 51 insertions(+), 23 deletions(-) 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 f2d0d706f..b9162ae31 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 @@ -2459,43 +2459,71 @@ fn is_external_resource_edit_endpoint(endpoint: &str) -> bool { /// 从服务端 4xx 响应体里取出**可读的失败原因**,让用户看到的不是笼统的 HTTP 状态码。 /// -/// 平台错误体在不同路由上可能是 `{error:{message}}`、`{error:"文本"}`、`{details:{message}}` -/// 或顶层 `{message}`;这里按这个顺序兼容,并截断到 200 字符,避免把整段响应体塞进用户可见文案。 +/// 平台错误体在不同路由上可能是 `{error:{message,details:{message}}}`、`{error:"文本"}`、 +/// `{details:{message}}` 或顶层 `{message}`。**`details.message` 优先**:平台把所有 4xx 都 +/// 兜底成同一条通用文案(`http_error.rs` 的 `resolve_http_error`),真实原因写在 +/// `details.message` 里(例如「当前素材类型不支持图片快速编辑」),只读 `error.message` +/// 会让用户永远看到「请求参数不合法」。 async fn editor_api_rejection_reason(response: reqwest::Response) -> Option { let body = response.text().await.ok()?; + editor_api_rejection_reason_from_body(body.as_str()) +} + +fn editor_api_rejection_reason_from_body(body: &str) -> Option { let value: serde_json::Value = serde_json::from_str(body.trim()).ok()?; - let candidate = value - .get("error") - .and_then(|error| { - error - .get("message") - .and_then(|message| message.as_str()) - .or_else(|| error.as_str()) + let error = value.get("error"); + let details = value.get("details"); + let error_details = details.or_else(|| error.and_then(|error| error.get("details"))); + // 取值顺序:`error.details.message` → `details.message` → `error.message` → `error` 文本 + // → 顶层 `message`。前两者才是平台写进去的具体原因,后两者只是兜底。 + let candidate = error_details + .and_then(|error_details| error_details.get("message")) + .and_then(serde_json::Value::as_str) + .or_else(|| { + details + .and_then(|details| details.get("message")) + .and_then(serde_json::Value::as_str) }) .or_else(|| { - value - .get("details") - .and_then(|details| details.get("message")) - .and_then(|message| message.as_str()) + error + .and_then(|error| error.get("message")) + .and_then(serde_json::Value::as_str) }) - .or_else(|| value.get("message").and_then(|message| message.as_str()))?; - let trimmed = candidate.trim(); - if trimmed.is_empty() { + .or_else(|| error.and_then(serde_json::Value::as_str)) + .or_else(|| value.get("message").and_then(serde_json::Value::as_str))? + .trim(); + if candidate.is_empty() { return None; } - // 平台把所有 4xx 都兜底成同一条通用文案(`http_error.rs`),真实原因在 `error.code` 里, - // 因此稳定码必须一起透出,否则用户和排障者只能看到"请求参数不合法"。 + // 平台把所有 4xx 都兜底成同一条通用文案(`http_error.rs`),稳定码和 `details` 里的 + // provider / assetKind / mediaType 必须一起透出,否则排障者只能看到"请求参数不合法"。 + let detail_text = |key: &str| { + error_details + .and_then(|error_details| error_details.get(key)) + .and_then(serde_json::Value::as_str) + .map(str::trim) + .filter(|detail| !detail.is_empty()) + }; let code = value .get("error") .and_then(|error| error.get("code")) .and_then(|code| code.as_str()) .map(str::trim) .filter(|code| !code.is_empty() && *code != "BAD_REQUEST"); - let reason: String = trimmed.chars().take(200).collect(); - Some(match code { - Some(code) => format!("{reason}|错误码 {code}"), - None => reason, - }) + let mut reason: String = candidate.chars().take(200).collect(); + if let Some(provider) = detail_text("provider") { + reason.push_str(format!("|provider {provider}").as_str()); + } + if let Some(asset_kind) = detail_text("assetKind") { + reason.push_str(format!("|素材类型 {asset_kind}").as_str()); + } + if let Some(media_type) = detail_text("mediaType") { + reason.push_str(format!("|媒体类型 {media_type}").as_str()); + } + if let Some(code) = code { + reason.push_str(format!("|错误码 {code}").as_str()); + } + Some(reason) } async fn submit_resource_edit_remote(