diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index b4ba2bc9b..c4576087b 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -2227,14 +2227,16 @@ async fn remove_editor_generated_screen_background_with_bgfilter( audit: &crate::external_api_audit::ExternalApiAuditContext, ) -> Result { if let Some(remaining) = editor_bgfilter_circuit_open_remaining(state) { + // 熔断打开只跳过 BgFilter 调用本身,仍要走阿里云兜底;绝不能在 cooldown 内直接退化到 + // 低质量本地扣色,否则整段冷却期从「双供应商降级」退化成单一本地键色扣除。 tracing::warn!( provider = "bgfilter", screen_color = screen_color.hex, seg_model, cooldown_ms = remaining.as_millis() as u64, - "editor_bgfilter_circuit_open_fallback_to_local_screen_background_removal" + "editor_bgfilter_circuit_open_fallback_to_aliyun_matting" ); - return remove_editor_generated_green_screen_background(image, screen_color); + return fallback_editor_screen_background_removal(state, image, screen_color, audit).await; } match request_editor_generated_screen_background_with_bgfilter( @@ -2273,39 +2275,50 @@ async fn remove_editor_generated_screen_background_with_bgfilter( crate::external_api_audit::matting_failure_audit_raw_excerpt(&error), ) .await; - match crate::aliyun_matting::segment_image_with_aliyun_matting( + fallback_editor_screen_background_removal(state, image, screen_color, audit).await + } + } +} + +/// BgFilter 不可用(熔断打开或调用失败)时的统一兜底链:先阿里云通用抠图,失败再退本地键色扣除。 +/// 熔断打开路径与 BgFilter 调用失败路径共用此链,确保 cooldown 内仍是「阿里云 → 本地」而非直接本地。 +async fn fallback_editor_screen_background_removal( + state: &AppState, + image: &DownloadedOpenAiImage, + screen_color: EditorScreenBackgroundColor, + audit: &crate::external_api_audit::ExternalApiAuditContext, +) -> Result { + match crate::aliyun_matting::segment_image_with_aliyun_matting( + state, + image, + "editor-screen-background", + ) + .await + { + Ok(image) => Ok(image), + Err(error) => { + tracing::warn!( + provider = "aliyun-matting", + screen_color = screen_color.hex, + error = %error, + error_details = ?error.details(), + "editor_aliyun_matting_fallback_to_local_screen_background_removal" + ); + crate::external_api_audit::record_matting_external_api_failure( state, - image, - "editor-screen-background", + audit, + "aliyun-matting", + state.config.aliyun_matting_endpoint.clone(), + "editor-screen-background-removal", + "aliyun_segment", + crate::external_api_audit::matting_failure_audit_status_code(&error), + crate::external_api_audit::matting_failure_audit_timeout(&error), + crate::external_api_audit::matting_failure_audit_latency_ms(&error), + error.message().to_string(), + crate::external_api_audit::matting_failure_audit_raw_excerpt(&error), ) - .await - { - Ok(image) => Ok(image), - Err(error) => { - tracing::warn!( - provider = "aliyun-matting", - screen_color = screen_color.hex, - error = %error, - error_details = ?error.details(), - "editor_aliyun_matting_fallback_to_local_screen_background_removal" - ); - crate::external_api_audit::record_matting_external_api_failure( - state, - audit, - "aliyun-matting", - state.config.aliyun_matting_endpoint.clone(), - "editor-screen-background-removal", - "aliyun_segment", - crate::external_api_audit::matting_failure_audit_status_code(&error), - crate::external_api_audit::matting_failure_audit_timeout(&error), - crate::external_api_audit::matting_failure_audit_latency_ms(&error), - error.message().to_string(), - crate::external_api_audit::matting_failure_audit_raw_excerpt(&error), - ) - .await; - remove_editor_generated_green_screen_background(image, screen_color) - } - } + .await; + remove_editor_generated_green_screen_background(image, screen_color) } } } @@ -7573,12 +7586,25 @@ mod tests { "remove_editor_generated_screen_background_with_bgfilter", ], ); - assert_function_contains( + assert_function_contains_in_order( source, "async fn remove_editor_generated_screen_background_with_bgfilter", + "async fn fallback_editor_screen_background_removal", + &[ + // 熔断打开分支必须先委派统一兜底链(阿里云→本地),不能直接退化本地扣色; + // BgFilter 调用失败分支同样走该兜底链。 + "editor_bgfilter_circuit_open_remaining", + "fallback_editor_screen_background_removal", + "request_editor_generated_screen_background_with_bgfilter", + "fallback_editor_screen_background_removal", + ], + ); + assert_function_contains_in_order( + source, + "async fn fallback_editor_screen_background_removal", "async fn request_editor_generated_screen_background_with_bgfilter", &[ - "request_editor_generated_screen_background_with_bgfilter", + "segment_image_with_aliyun_matting", "remove_editor_generated_green_screen_background", ], );