From cb1dd78e1d97915ad125925cc77eaa9919ea039a Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 10 Jul 2026 08:47:21 +0000 Subject: [PATCH] =?UTF-8?q?=E9=98=BF=E9=87=8C=E4=BA=91=E6=8A=A0=E5=9B=BE?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E9=A2=84=E6=A3=80=E5=A4=B1=E8=B4=A5=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=AF=AF=E6=8A=A5=E4=B8=BA=E5=8F=AF=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E6=95=85=E9=9A=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InvalidConfig/InvalidRequest/Sign 都是发请求前的本地预检失败(未配置、图片解码失败、 尺寸过小、签名构造失败),此时根本没调用阿里云。旧映射统一默认 externalCallAttempted=true、 transport=true,把本地失败写成可重试的 transport 供应商故障审计。 现按 MattingError 变体区分:仅 Upstream 保留原上游失败语义,本地预检标记 externalCallAttempted=false / transport=false,调用方据此跳过外部失败审计。 Co-Authored-By: Claude Opus 4.8 --- .../crates/api-server/src/aliyun_matting.rs | 96 +++++++++++++++---- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/server-rs/crates/api-server/src/aliyun_matting.rs b/server-rs/crates/api-server/src/aliyun_matting.rs index cf2e9a4e0..a2c41ccda 100644 --- a/server-rs/crates/api-server/src/aliyun_matting.rs +++ b/server-rs/crates/api-server/src/aliyun_matting.rs @@ -4,6 +4,7 @@ //! 统一由 platform-matting 上传 VIAPI 官方临时桶(1 天自动过期,无需清理)。 use axum::http::StatusCode; +use platform_matting::MattingError; use serde_json::json; use crate::{ @@ -27,23 +28,7 @@ pub(crate) async fn segment_image_with_aliyun_matting( .segment_image_to_transparent_png(image.bytes.as_slice(), &file_name) .await .map_err(|error| { - let message = error.message(); - let timeout = aliyun_matting_error_is_timeout(message); - let upstream_status = aliyun_matting_error_http_status(message); - let status = if timeout { - StatusCode::GATEWAY_TIMEOUT - } else { - StatusCode::BAD_GATEWAY - }; - AppError::from_status(status).with_details(json!({ - "provider": "aliyun-matting", - "message": message, - "timeout": timeout, - "transport": aliyun_matting_error_is_transport(message), - "upstreamStatus": upstream_status, - "latencyMs": started_at.elapsed().as_millis() as u64, - "rawExcerpt": message.chars().take(500).collect::(), - })) + aliyun_matting_failure_to_app_error(&error, started_at.elapsed().as_millis() as u64) })?; tracing::info!( provider = "aliyun-matting", @@ -59,6 +44,47 @@ pub(crate) async fn segment_image_with_aliyun_matting( }) } +/// 把 platform-matting 的错误映射成审计友好的 AppError。 +/// +/// 关键区分:`InvalidConfig` / `InvalidRequest` / `Sign` 都是发请求前的本地预检失败 +/// (客户端未配置、图片解码失败、尺寸过小、签名构造失败),此时根本没调用阿里云,必须 +/// 标记 `externalCallAttempted=false`、`transport=false`,否则会被误写成可重试的供应商 +/// transport 故障审计。只有 `Upstream` 才是真实发生过的外部调用失败。 +fn aliyun_matting_failure_to_app_error(error: &MattingError, latency_ms: u64) -> AppError { + let message = error.message(); + match error { + MattingError::Upstream(_) => { + let timeout = aliyun_matting_error_is_timeout(message); + let upstream_status = aliyun_matting_error_http_status(message); + let status = if timeout { + StatusCode::GATEWAY_TIMEOUT + } else { + StatusCode::BAD_GATEWAY + }; + AppError::from_status(status).with_details(json!({ + "provider": "aliyun-matting", + "message": message, + "timeout": timeout, + "transport": aliyun_matting_error_is_transport(message), + "upstreamStatus": upstream_status, + "latencyMs": latency_ms, + "rawExcerpt": message.chars().take(500).collect::(), + })) + } + MattingError::InvalidConfig(_) | MattingError::InvalidRequest(_) | MattingError::Sign(_) => { + AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({ + "provider": "aliyun-matting", + "message": message, + "timeout": false, + "transport": false, + "externalCallAttempted": false, + "latencyMs": latency_ms, + "rawExcerpt": message.chars().take(500).collect::(), + })) + } + } +} + fn aliyun_matting_error_is_timeout(message: &str) -> bool { let normalized = message.to_ascii_lowercase(); normalized.contains("timeout") || normalized.contains("timed out") @@ -134,4 +160,40 @@ mod tests { assert!(!crate::external_api_audit::matting_failure_external_call_attempted(&error)); } + + #[test] + fn local_preflight_failures_do_not_count_as_external_call() { + for error in [ + MattingError::InvalidRequest( + "待抠图图片尺寸 16x16 过小,阿里云通用抠图要求每条边大于 32 像素".to_string(), + ), + MattingError::InvalidRequest("解析待抠图图片失败:invalid png".to_string()), + MattingError::InvalidConfig("endpoint 为空".to_string()), + MattingError::Sign("初始化 OSS V1 签名器失败".to_string()), + ] { + let mapped = aliyun_matting_failure_to_app_error(&error, 3); + + assert!( + !crate::external_api_audit::matting_failure_external_call_attempted(&mapped), + "本地预检失败不应记为外部调用:{}", + error.message() + ); + let details = mapped.details().expect("details present"); + assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false)); + assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false)); + } + } + + #[test] + fn upstream_failure_still_counts_as_external_call() { + let error = MattingError::Upstream( + "通用抠图请求失败:dns error".to_string(), + ); + let mapped = aliyun_matting_failure_to_app_error(&error, 12); + + assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped)); + let details = mapped.details().expect("details present"); + // 无 HTTP 状态的上游失败仍应标记为可重试 transport 故障。 + assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true)); + } }