修正 AGC 快速编辑 4xx 失败原因取值:优先 details.message 并附 provider 与素材类型上下文

- `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` 并补定向用例,钉住取值顺序、截断与兜底行为
This commit is contained in:
2026-09-10 19:01:09 +08:00
parent 8c74e5f97c
commit c20e28bed0
@@ -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<String> {
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<String> {
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(