修复 BgFilter 熔断打开时绕过阿里云兜底直接本地扣色

circuit open 时直接 return 本地键色扣除,使同函数后面的阿里云兜底不可达;
BgFilter 一旦连续失败触发 cooldown,整段冷却期从「BgFilter→阿里云→本地」
双供应商降级退化成只剩低质量本地扣色。

- 抽出统一兜底链 fallback_editor_screen_background_removal(阿里云→本地),
  熔断打开与 BgFilter 调用失败两条路径共用:熔断只跳过 BgFilter HTTP 调用本身,
  仍先试阿里云。阿里云失败审计逻辑原样搬入 helper,零行为改动。
- 日志 tag 改为 editor_bgfilter_circuit_open_fallback_to_aliyun_matting,如实反映。
- 结构测试升级为 in-order 断言,钉死「熔断→fallback→请求→fallback」与
  helper 内「阿里云→本地」顺序,防止回退成直接本地扣色。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 10:16:26 +00:00
parent e15e86729e
commit 778b94e0ea
@@ -2227,14 +2227,16 @@ async fn remove_editor_generated_screen_background_with_bgfilter(
audit: &crate::external_api_audit::ExternalApiAuditContext,
) -> Result<DownloadedOpenAiImage, AppError> {
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<DownloadedOpenAiImage, AppError> {
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",
],
);