补齐抠图参数透传与验收
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m4s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 3m51s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m45s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m15s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m9s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m50s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / Frontend tests (pull_request) Successful in 6m9s
Project CI / AI game creator shell web tests (pull_request) Successful in 9m8s
Project CI / Native shell tests (pull_request) Successful in 12m36s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m4s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 3m51s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m45s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m15s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m9s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m50s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / Frontend tests (pull_request) Successful in 6m9s
Project CI / AI game creator shell web tests (pull_request) Successful in 9m8s
Project CI / Native shell tests (pull_request) Successful in 12m36s
修复flat自动识别门禁并严格校验模式和颜色 保持旧请求幂等指纹并区分客户端抠图意图 同步工具说明和Skill契约并补充定向测试 记录真实BgFilter验证及本地数据库阻塞的待验收项
This commit is contained in:
@@ -849,13 +849,13 @@ fn validate_internal_request(
|
||||
}
|
||||
match request.background_mode {
|
||||
BgfilterBackgroundMode::Flat => {
|
||||
let screen_color = request.screen_color.as_deref().ok_or_else(|| {
|
||||
WorkerFailure::new("invalid_request", "flat 请求缺少 screenColor", false)
|
||||
})?;
|
||||
if !valid_screen_color(screen_color) {
|
||||
if let Some(screen_color) = request.screen_color.as_deref()
|
||||
&& screen_color != "auto"
|
||||
&& !valid_screen_color(screen_color)
|
||||
{
|
||||
return Err(WorkerFailure::new(
|
||||
"invalid_request",
|
||||
"screenColor 必须是 #RRGGBB",
|
||||
"screenColor 必须是 auto 或 #RRGGBB",
|
||||
false,
|
||||
));
|
||||
}
|
||||
@@ -2811,7 +2811,13 @@ mod tests {
|
||||
call_budget_ms: 321_000,
|
||||
audit_context: None,
|
||||
};
|
||||
assert!(validate_internal_request(&request, &admission).is_err());
|
||||
assert!(validate_internal_request(&request, &admission).is_ok());
|
||||
request.screen_color = Some("auto".to_string());
|
||||
assert!(validate_internal_request(&request, &admission).is_ok());
|
||||
for invalid in ["", "AUTO", "#12", "#GGGGGG"] {
|
||||
request.screen_color = Some(invalid.to_string());
|
||||
assert!(validate_internal_request(&request, &admission).is_err());
|
||||
}
|
||||
request.screen_color = Some("#CFEFFF".to_string());
|
||||
assert!(validate_internal_request(&request, &admission).is_ok());
|
||||
// callBudget 只是父侧配置指纹:与 worker 公式值不一致不得拒绝(发布重启窗口
|
||||
@@ -2854,6 +2860,85 @@ mod tests {
|
||||
assert!(validate_internal_request(&request, &admission).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_removal_provider_multipart_preserves_mode_and_color() {
|
||||
for (mode, color) in [
|
||||
(BgfilterBackgroundMode::Complex, None),
|
||||
(BgfilterBackgroundMode::Flat, None),
|
||||
(BgfilterBackgroundMode::Flat, Some("auto")),
|
||||
(BgfilterBackgroundMode::Flat, Some("#Ab12EF")),
|
||||
] {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
|
||||
let router = Router::new().route(
|
||||
"/remove-background",
|
||||
post(move |headers: HeaderMap, body: axum::body::Bytes| {
|
||||
let sender = sender.clone();
|
||||
async move {
|
||||
sender.send((headers, body)).await.unwrap();
|
||||
(
|
||||
[(axum::http::header::CONTENT_TYPE, "image/png")],
|
||||
encoded_png(2, 3),
|
||||
)
|
||||
}
|
||||
}),
|
||||
);
|
||||
let server = tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
|
||||
let state = AppState::new(AppConfig {
|
||||
editor_bgfilter_base_url: format!("http://{address}"),
|
||||
editor_bgfilter_token: Some("provider-test-token".to_string()),
|
||||
..AppConfig::default()
|
||||
})
|
||||
.unwrap();
|
||||
let request = BgfilterInternalRequest {
|
||||
request_id: "multipart-test".to_string(),
|
||||
source_object_key: "editor-upload/source.png".to_string(),
|
||||
background_mode: mode,
|
||||
screen_color: color.map(str::to_string),
|
||||
seg_model: "birefnet".to_string(),
|
||||
cross_check: false,
|
||||
max_queue_wait_ms: 1000,
|
||||
call_budget_ms: 321000,
|
||||
audit_context: None,
|
||||
};
|
||||
let result = request_provider_once(
|
||||
&state,
|
||||
&request,
|
||||
"https://example.invalid/source.png",
|
||||
ProviderAttemptBudget {
|
||||
timeout: Duration::from_secs(5),
|
||||
budget_limited: false,
|
||||
},
|
||||
1,
|
||||
Instant::now() + Duration::from_secs(5),
|
||||
&BgfilterTaskTracker::new(),
|
||||
)
|
||||
.await;
|
||||
server.abort();
|
||||
let result = result.unwrap();
|
||||
assert_eq!((result.width, result.height), (2, 3));
|
||||
let (headers, body) = receiver.recv().await.unwrap();
|
||||
assert_eq!(
|
||||
headers[BGFILTER_PROVIDER_TOKEN_HEADER],
|
||||
"provider-test-token"
|
||||
);
|
||||
let body = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(body.contains(&format!(
|
||||
"name=\"background_mode\"\r\n\r\n{}\r\n",
|
||||
mode.as_str()
|
||||
)));
|
||||
assert!(body.contains("name=\"image_url\""));
|
||||
assert!(!body.contains("name=\"file\""));
|
||||
match color {
|
||||
Some(color) => {
|
||||
assert!(body.contains(&format!("name=\"screen_color\"\r\n\r\n{color}\r\n")))
|
||||
}
|
||||
None => assert!(!body.contains("name=\"screen_color\"")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_timeout_distinguishes_full_attempt_from_budget_truncation() {
|
||||
let now = Instant::now();
|
||||
|
||||
@@ -1019,6 +1019,52 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_removal_identity_preserves_old_dto_and_distinguishes_options() {
|
||||
let legacy = json!({
|
||||
"sourceImageSrc": "resource-source", "projectId": null,
|
||||
"targetLayerId": null, "assetKind": null, "generationInputs": null,
|
||||
"assetFolderId": null, "assetLabel": null, "sourceResourceId": null,
|
||||
"taskId": null, "canvasCompletion": null,
|
||||
});
|
||||
let old = external_api_editor_generation_request_identity(
|
||||
"user-1",
|
||||
EDITOR_BACKGROUND_REMOVAL_JOB_KIND,
|
||||
&legacy,
|
||||
"stable-key",
|
||||
)
|
||||
.unwrap();
|
||||
let restored: crate::editor_project::EditorBackgroundRemovalRequest =
|
||||
serde_json::from_value(legacy.clone()).unwrap();
|
||||
let current = external_api_editor_generation_request_identity(
|
||||
"user-1",
|
||||
EDITOR_BACKGROUND_REMOVAL_JOB_KIND,
|
||||
&restored,
|
||||
"stable-key",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(old.request_fingerprint, current.request_fingerprint);
|
||||
assert_eq!(old.job_id, current.job_id);
|
||||
let mut fingerprints = std::collections::HashSet::new();
|
||||
fingerprints.insert(current.request_fingerprint);
|
||||
for color in [None, Some("auto"), Some("#CFEFFF"), Some("#112233")] {
|
||||
let mut body = legacy.clone();
|
||||
body["backgroundMode"] = json!("flat");
|
||||
if let Some(color) = color {
|
||||
body["screenColor"] = json!(color);
|
||||
}
|
||||
let changed = external_api_editor_generation_request_identity(
|
||||
"user-1",
|
||||
EDITOR_BACKGROUND_REMOVAL_JOB_KIND,
|
||||
&body,
|
||||
"stable-key",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(old.job_id, changed.job_id);
|
||||
assert!(fingerprints.insert(changed.request_fingerprint));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_api_dedupe_key_preserves_legacy_hash_bytes() {
|
||||
let dedupe_key = build_editor_generation_dedupe_key(
|
||||
|
||||
@@ -452,7 +452,9 @@ pub struct EditorImageEditRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorBackgroundRemovalRequest {
|
||||
pub(crate) source_image_src: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) background_mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) screen_color: Option<String>,
|
||||
pub(crate) project_id: Option<String>,
|
||||
pub(crate) target_layer_id: Option<String>,
|
||||
@@ -6292,7 +6294,6 @@ pub(crate) async fn enqueue_editor_background_removal_for_owner(
|
||||
mut payload: EditorBackgroundRemovalRequest,
|
||||
external_idempotency_key: Option<&str>,
|
||||
) -> Result<ExternalGenerationJobRecord, AppError> {
|
||||
normalize_editor_background_removal_options(&mut payload)?;
|
||||
let external_request_identity = external_idempotency_key
|
||||
.map(|idempotency_key| {
|
||||
external_api_editor_generation_request_identity(
|
||||
@@ -6303,6 +6304,7 @@ pub(crate) async fn enqueue_editor_background_removal_for_owner(
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
normalize_editor_background_removal_options(&mut payload)?;
|
||||
ensure_editor_reference_image_source_is_stable(
|
||||
payload.source_image_src.as_str(),
|
||||
"editor-background-removal",
|
||||
@@ -6409,34 +6411,28 @@ fn normalize_editor_background_removal_options(
|
||||
) -> Result<(), AppError> {
|
||||
let mode = payload
|
||||
.background_mode
|
||||
.take()
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
.filter(|value| !value.is_empty())
|
||||
.clone()
|
||||
.unwrap_or_else(|| "complex".to_string());
|
||||
if mode != "complex" && mode != "flat" {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-background-removal",
|
||||
"field": "backgroundMode",
|
||||
"message": "backgroundMode must be complex or flat",
|
||||
"message": "backgroundMode 必须是 complex 或 flat",
|
||||
})),
|
||||
);
|
||||
}
|
||||
let color = payload
|
||||
.screen_color
|
||||
.take()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
let color = payload.screen_color.as_deref();
|
||||
if mode == "complex" && color.is_some() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-background-removal",
|
||||
"field": "screenColor",
|
||||
"message": "screenColor is only supported when backgroundMode is flat",
|
||||
"message": "只有 flat 模式可以提供 screenColor",
|
||||
})),
|
||||
);
|
||||
}
|
||||
if let Some(color) = color.as_deref()
|
||||
if let Some(color) = color
|
||||
&& color != "auto"
|
||||
&& !(color.len() == 7
|
||||
&& color.starts_with('#')
|
||||
@@ -6448,12 +6444,11 @@ fn normalize_editor_background_removal_options(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-background-removal",
|
||||
"field": "screenColor",
|
||||
"message": "screenColor must be auto or #RRGGBB",
|
||||
"message": "screenColor 必须是 auto 或 #RRGGBB",
|
||||
})),
|
||||
);
|
||||
}
|
||||
payload.background_mode = Some(mode);
|
||||
payload.screen_color = color;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6706,6 +6701,7 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
||||
caller: EditorGenerationCaller,
|
||||
mut payload: EditorBackgroundRemovalRequest,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
normalize_editor_background_removal_options(&mut payload)?;
|
||||
payload.generation_inputs =
|
||||
sanitize_editor_client_generation_inputs(payload.generation_inputs.take());
|
||||
let started_at = Instant::now();
|
||||
@@ -8010,7 +8006,7 @@ async fn request_editor_background_removal_image_with_bgfilter_worker(
|
||||
screen_color: Option<&str>,
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
) -> Result<EditorBackgroundRemovalImage, AppError> {
|
||||
// complex 没有 flat fallback,排队上限只预留 2s 传输窗;worker 重启窗口内的
|
||||
// 独立抠图两种模式都不进入生成链路的 fallback,排队上限只预留 2s 传输窗;worker 重启窗口内的
|
||||
// 连接失败由 client 内部按预算有界重试,避免 max_attempts=1 的队列任务终态失败。
|
||||
let mode = match background_mode {
|
||||
"flat" => crate::bgfilter_worker::BgfilterBackgroundMode::Flat,
|
||||
@@ -13717,6 +13713,110 @@ pub(crate) fn current_utc_micros() -> i64 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn background_removal_options_preserve_queue_parameters_and_legacy_identity() {
|
||||
for (fields, mode, color) in [
|
||||
(json!({}), "complex", None),
|
||||
(json!({"backgroundMode": "complex"}), "complex", None),
|
||||
(json!({"backgroundMode": "flat"}), "flat", None),
|
||||
(
|
||||
json!({"backgroundMode": "flat", "screenColor": "auto"}),
|
||||
"flat",
|
||||
Some("auto"),
|
||||
),
|
||||
(
|
||||
json!({"backgroundMode": "flat", "screenColor": "#Ab12EF"}),
|
||||
"flat",
|
||||
Some("#Ab12EF"),
|
||||
),
|
||||
] {
|
||||
let mut input = json!({"sourceImageSrc": "resource-source"});
|
||||
input
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(fields.as_object().unwrap().clone());
|
||||
let mut payload: EditorBackgroundRemovalRequest =
|
||||
serde_json::from_value(input).unwrap();
|
||||
if payload.background_mode.is_none() {
|
||||
let raw = serde_json::to_value(&payload).unwrap();
|
||||
assert!(raw.get("backgroundMode").is_none());
|
||||
assert!(raw.get("screenColor").is_none());
|
||||
}
|
||||
normalize_editor_background_removal_options(&mut payload).unwrap();
|
||||
let restored: EditorBackgroundRemovalRequest =
|
||||
serde_json::from_str(&serde_json::to_string(&payload).unwrap()).unwrap();
|
||||
assert_eq!(restored.background_mode.as_deref(), Some(mode));
|
||||
assert_eq!(restored.screen_color.as_deref(), color);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_removal_options_reject_invalid_values_and_color_without_flat() {
|
||||
for fields in [
|
||||
json!({"backgroundMode": ""}),
|
||||
json!({"backgroundMode": "FLAT"}),
|
||||
json!({"backgroundMode": " flat "}),
|
||||
json!({"backgroundMode": "other"}),
|
||||
json!({"screenColor": "auto"}),
|
||||
json!({"backgroundMode": "complex", "screenColor": "auto"}),
|
||||
json!({"backgroundMode": "complex", "screenColor": ""}),
|
||||
json!({"backgroundMode": "flat", "screenColor": ""}),
|
||||
json!({"backgroundMode": "flat", "screenColor": "AUTO"}),
|
||||
json!({"backgroundMode": "flat", "screenColor": " auto "}),
|
||||
json!({"backgroundMode": "flat", "screenColor": "#GGGGGG"}),
|
||||
json!({"backgroundMode": "flat", "screenColor": "CFEFFF"}),
|
||||
] {
|
||||
let mut input = json!({"sourceImageSrc": "resource-source"});
|
||||
input
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(fields.as_object().unwrap().clone());
|
||||
let mut payload: EditorBackgroundRemovalRequest =
|
||||
serde_json::from_value(input).unwrap();
|
||||
assert_eq!(
|
||||
normalize_editor_background_removal_options(&mut payload)
|
||||
.unwrap_err()
|
||||
.status_code(),
|
||||
StatusCode::BAD_REQUEST,
|
||||
"{fields}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_removal_flat_options_reach_internal_worker_unchanged() {
|
||||
for color in [None, Some("auto"), Some("#Ab12EF")] {
|
||||
let response_png = encode_test_png(3, 2);
|
||||
let (base_url, receiver, server) = spawn_bgfilter_worker_png_mock(response_png.clone());
|
||||
let state = AppState::new(AppConfig {
|
||||
bgfilter_worker_base_url: base_url,
|
||||
bgfilter_internal_token: Some("flat-test-token".to_string()),
|
||||
..AppConfig::default()
|
||||
})
|
||||
.unwrap();
|
||||
let audit = crate::external_api_audit::ExternalApiAuditContext {
|
||||
user_id: None,
|
||||
profile_id: None,
|
||||
request_id: None,
|
||||
external_call_deadline: None,
|
||||
};
|
||||
let output = request_editor_background_removal_image_with_bgfilter_worker(
|
||||
&state,
|
||||
"generated-character-drafts/editor/source.png",
|
||||
"flat",
|
||||
color,
|
||||
&audit,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let request = receiver.recv_timeout(Duration::from_secs(1)).unwrap();
|
||||
server.join().unwrap();
|
||||
let payload = parse_mock_http_json_body(&request);
|
||||
assert_eq!(payload["backgroundMode"], json!("flat"));
|
||||
assert_eq!(payload["screenColor"], json!(color));
|
||||
assert_eq!(output.image.bytes, response_png);
|
||||
}
|
||||
}
|
||||
use super::*;
|
||||
use crate::{
|
||||
config::AppConfig,
|
||||
|
||||
@@ -1868,7 +1868,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn external_background_removal_rejects_undocumented_fields_before_queueing() {
|
||||
async fn external_background_removal_rejects_invalid_parameters_before_queueing() {
|
||||
let state = AppState::new(crate::config::AppConfig::default())
|
||||
.expect("external background removal test state should build");
|
||||
state.fail_test_editor_generation_enqueue();
|
||||
@@ -1890,6 +1890,26 @@ mod tests {
|
||||
json!({"taskId": "caller-controlled-task"}),
|
||||
),
|
||||
("unknown field", json!({"unexpected": true})),
|
||||
("invalid mode", json!({"backgroundMode": "unknown"})),
|
||||
("empty mode", json!({"backgroundMode": ""})),
|
||||
("uppercase mode", json!({"backgroundMode": "FLAT"})),
|
||||
("color without flat", json!({"screenColor": "auto"})),
|
||||
(
|
||||
"complex with color",
|
||||
json!({"backgroundMode": "complex", "screenColor": "#123456"}),
|
||||
),
|
||||
(
|
||||
"empty color",
|
||||
json!({"backgroundMode": "flat", "screenColor": ""}),
|
||||
),
|
||||
(
|
||||
"invalid color",
|
||||
json!({"backgroundMode": "flat", "screenColor": "red"}),
|
||||
),
|
||||
(
|
||||
"non-string color",
|
||||
json!({"backgroundMode": "flat", "screenColor": 123}),
|
||||
),
|
||||
] {
|
||||
let mut request_body = json!({"sourceImageSrc": "editor-upload/source.png"});
|
||||
request_body
|
||||
@@ -2848,6 +2868,25 @@ mod tests {
|
||||
["EditorBackgroundRemovalRequest"]["properties"]["targetLayerId"]["description"]
|
||||
.as_str()
|
||||
.expect("background removal targetLayerId should document placement semantics");
|
||||
let background_properties =
|
||||
&parsed["components"]["schemas"]["EditorBackgroundRemovalRequest"]["properties"];
|
||||
assert_eq!(
|
||||
background_properties["backgroundMode"]["enum"],
|
||||
json!(["complex", "flat", null])
|
||||
);
|
||||
assert_eq!(
|
||||
background_properties["backgroundMode"]["default"],
|
||||
"complex"
|
||||
);
|
||||
assert_eq!(
|
||||
background_properties["screenColor"]["pattern"],
|
||||
"^(auto|#[0-9A-Fa-f]{6})$"
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorBackgroundRemovalRequest"]["then"]["properties"]
|
||||
["backgroundMode"]["const"],
|
||||
"flat"
|
||||
);
|
||||
assert!(background_target_description.contains("projectId"));
|
||||
assert!(background_target_description.contains("canvasCompletion"));
|
||||
assert!(background_target_description.contains("assetObjectId"));
|
||||
|
||||
Reference in New Issue
Block a user