diff --git a/server-rs/crates/api-server/src/aliyun_matting.rs b/server-rs/crates/api-server/src/aliyun_matting.rs index d3c684d57..60f5a9921 100644 --- a/server-rs/crates/api-server/src/aliyun_matting.rs +++ b/server-rs/crates/api-server/src/aliyun_matting.rs @@ -118,22 +118,36 @@ mod tests { 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)); + 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_transport_failure_maps_to_retryable_transport() { - let error = - MattingError::upstream_transport_error("通用抠图请求失败:dns error".to_string(), false); + let error = MattingError::upstream_transport_error( + "通用抠图请求失败:dns error".to_string(), + false, + ); 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 故障,且不带 upstreamStatus。 - assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false)); + assert_eq!( + details.get("transport").and_then(|v| v.as_bool()), + Some(true) + ); + assert_eq!( + details.get("timeout").and_then(|v| v.as_bool()), + Some(false) + ); assert!(details.get("upstreamStatus").is_some_and(|v| v.is_null())); } @@ -146,7 +160,10 @@ mod tests { assert_eq!(mapped.status_code(), StatusCode::GATEWAY_TIMEOUT); let details = mapped.details().expect("details present"); assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true)); + assert_eq!( + details.get("transport").and_then(|v| v.as_bool()), + Some(true) + ); } #[test] @@ -159,7 +176,13 @@ mod tests { assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped)); let details = mapped.details().expect("details present"); - assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false)); - assert_eq!(details.get("upstreamStatus").and_then(|v| v.as_u64()), Some(429)); + assert_eq!( + details.get("transport").and_then(|v| v.as_bool()), + Some(false) + ); + assert_eq!( + details.get("upstreamStatus").and_then(|v| v.as_u64()), + Some(429) + ); } } diff --git a/server-rs/crates/api-server/src/editor_green_screen.rs b/server-rs/crates/api-server/src/editor_green_screen.rs index 5840533de..c13d3abc1 100644 --- a/server-rs/crates/api-server/src/editor_green_screen.rs +++ b/server-rs/crates/api-server/src/editor_green_screen.rs @@ -109,7 +109,6 @@ pub(crate) fn default_editor_screen_background_color() -> EditorScreenBackground EDITOR_SCREEN_BACKGROUND_COLORS[0] } - pub(crate) fn parse_editor_screen_background_color( value: Option<&str>, ) -> Result { diff --git a/server-rs/crates/api-server/src/editor_screen_background_decision.rs b/server-rs/crates/api-server/src/editor_screen_background_decision.rs index 77e3dcc2c..ac38816c9 100644 --- a/server-rs/crates/api-server/src/editor_screen_background_decision.rs +++ b/server-rs/crates/api-server/src/editor_screen_background_decision.rs @@ -177,16 +177,15 @@ pub(crate) async fn resolve_editor_screen_background_color( let mut last_error: Option = None; for attempt in 1..=EDITOR_SCREEN_BACKGROUND_DECISION_MAX_ATTEMPTS { let user_message = match source_image_data_url { - Some(image_url) => { - LlmMessage::user(user_prompt.as_str()).with_image_url(image_url) - } + Some(image_url) => LlmMessage::user(user_prompt.as_str()).with_image_url(image_url), None => LlmMessage::user(user_prompt.as_str()), }; // 预算要够推理模型(如 gpt-5-mini)先花几百 token 推理、再吐 JSON 答案; // 实测 low 档推理约 320~384 token,取 1024 留足余量。降级客户端遇 stop 提前结束,不会多花。 - let mut request = LlmTextRequest::new(vec![LlmMessage::system(system_prompt), user_message]) - .with_max_tokens(1024) - .with_request_timeout_ms(EDITOR_SCREEN_BACKGROUND_DECISION_TIMEOUT_MS); + let mut request = + LlmTextRequest::new(vec![LlmMessage::system(system_prompt), user_message]) + .with_max_tokens(1024) + .with_request_timeout_ms(EDITOR_SCREEN_BACKGROUND_DECISION_TIMEOUT_MS); if let Some(decision_model) = decision_model { // gpt-5-mini 是推理模型(有图视觉档 / 无图文本档均适用):走 Responses 协议并压到 low // 推理档,否则默认档会把预算全烧在推理上、返回空答案。 @@ -296,8 +295,7 @@ async fn record_editor_screen_background_decision_llm_error( prompt_chars: usize, reference_image_count: usize, ) { - let (failure_stage, status_code, timeout, retryable, error_source, raw_excerpt) = match error - { + let (failure_stage, status_code, timeout, retryable, error_source, raw_excerpt) = match error { LlmError::InvalidConfig(_) | LlmError::InvalidRequest(_) => return, LlmError::Timeout { .. } => ("request_timeout", None, true, true, None, None), LlmError::Connectivity { message, .. } => ( @@ -329,14 +327,9 @@ async fn record_editor_screen_background_decision_llm_error( ), LlmError::EmptyResponse => ("missing_response", Some(200), false, false, None, None), LlmError::StreamUnavailable => ("response_body", Some(200), false, true, None, None), - LlmError::Transport(message) => ( - "transport", - None, - false, - true, - Some(message.as_str()), - None, - ), + LlmError::Transport(message) => { + ("transport", None, false, true, Some(message.as_str()), None) + } }; record_editor_screen_background_decision_failure( audit, @@ -754,8 +747,7 @@ mod tests { request_id: Some("request-1".to_string()), }, ); - let tracking = - crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); + let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); assert_eq!(audit.provider, "vector-engine"); assert_eq!(audit.endpoint, "https://vector.example/v1/responses"); @@ -809,8 +801,7 @@ mod tests { 1, &ExternalApiAuditContext::default(), ); - let tracking = - crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); + let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); assert_eq!(audit.failure_stage, "request_timeout"); assert_eq!(audit.status_code, None); @@ -897,7 +888,8 @@ mod tests { let (k, v) = trimmed.split_once('=').unwrap(); let v = v.trim().trim_matches('"').trim_matches('\''); // 先出现的文件优先(.env.local > .env.secrets.local > .env),与服务端 dotenv 顺序一致。 - map.entry(k.trim().to_string()).or_insert_with(|| v.to_string()); + map.entry(k.trim().to_string()) + .or_insert_with(|| v.to_string()); } } std::env::var(key).ok().or_else(|| map.get(key).cloned()) @@ -934,7 +926,10 @@ mod tests { let image = RgbaImage::from_pixel(64, 64, Rgba([120, 180, 120, 255])); let mut bytes = Vec::new(); image::DynamicImage::ImageRgba8(image) - .write_to(&mut std::io::Cursor::new(&mut bytes), image::ImageFormat::Png) + .write_to( + &mut std::io::Cursor::new(&mut bytes), + image::ImageFormat::Png, + ) .expect("test image should encode"); format!( "data:image/png;base64,{}", @@ -966,7 +961,11 @@ mod tests { eprintln!( "[live 无图] mode={:?} hex={} label={} attempts={} fallback={}", - decision.mode, decision.color.hex, decision.color.label, decision.attempts, decision.fallback + decision.mode, + decision.color.hex, + decision.color.label, + decision.attempts, + decision.fallback ); assert_eq!(decision.mode, EditorScreenBackgroundDecisionMode::Auto); assert!( @@ -1000,7 +999,11 @@ mod tests { eprintln!( "[live 有图] mode={:?} hex={} label={} attempts={} fallback={}", - decision.mode, decision.color.hex, decision.color.label, decision.attempts, decision.fallback + decision.mode, + decision.color.hex, + decision.color.label, + decision.attempts, + decision.fallback ); assert_eq!(decision.mode, EditorScreenBackgroundDecisionMode::Auto); assert!( diff --git a/server-rs/crates/api-server/src/editor_screen_background_filter.rs b/server-rs/crates/api-server/src/editor_screen_background_filter.rs index 59c70a521..d67d7bba0 100644 --- a/server-rs/crates/api-server/src/editor_screen_background_filter.rs +++ b/server-rs/crates/api-server/src/editor_screen_background_filter.rs @@ -203,11 +203,12 @@ impl ForegroundHistogram { .filter(|bin| bin.mass >= mass_floor) .filter_map(|bin| { let lab = bin.mean(); - (lab[0] > SKIN_MIN_LIGHTNESS && lab[1] > SKIN_MIN_A && lab[2] > SKIN_MIN_B) - .then(|| SkinReference { + (lab[0] > SKIN_MIN_LIGHTNESS && lab[1] > SKIN_MIN_A && lab[2] > SKIN_MIN_B).then( + || SkinReference { lab, rgb: bin.mean_rgb(), - }) + }, + ) }) .max_by(|left, right| { left.lab[0] @@ -481,12 +482,18 @@ mod tests { let report = report_for(&transparent_image_with_center_block([250, 224, 200]), true); assert!( - report.excluded.iter().any(|(color, _)| color.hex == "#FFD6C2"), + report + .excluded + .iter() + .any(|(color, _)| color.hex == "#FFD6C2"), "肤色前景应剔除暖浅桃色,excluded: {}", report.excluded_summary() ); assert!( - report.excluded.iter().any(|(color, _)| color.hex == "#FFF2A8"), + report + .excluded + .iter() + .any(|(color, _)| color.hex == "#FFF2A8"), "肤色前景应剔除淡黄(Rule 2/3 关键新覆盖),excluded: {}", report.excluded_summary() ); @@ -520,7 +527,10 @@ mod tests { let enabled = report_for(&image, true); assert!( - enabled.excluded.iter().any(|(color, _)| color.hex == "#FFF2A8"), + enabled + .excluded + .iter() + .any(|(color, _)| color.hex == "#FFF2A8"), "开启皮肤否决时淡黄应被剔除,excluded: {}", enabled.excluded_summary() ); @@ -542,7 +552,10 @@ mod tests { let report = report_for(&transparent_image_with_center_block([127, 179, 255]), false); assert!( - report.excluded.iter().any(|(color, _)| color.hex == "#7FB3FF"), + report + .excluded + .iter() + .any(|(color, _)| color.hex == "#7FB3FF"), "蓝色前景应剔除中度天蓝,excluded: {}", report.excluded_summary() ); diff --git a/server-rs/crates/platform-matting/examples/segment_smoke.rs b/server-rs/crates/platform-matting/examples/segment_smoke.rs index c778b5f02..a0c817402 100644 --- a/server-rs/crates/platform-matting/examples/segment_smoke.rs +++ b/server-rs/crates/platform-matting/examples/segment_smoke.rs @@ -38,17 +38,16 @@ async fn main() { }); let input_bytes = std::fs::read(&input_path) .unwrap_or_else(|error| panic!("读取测试图片失败({input_path}):{error}")); - println!("[1/5] 已读取测试图片:{input_path}({} 字节)", input_bytes.len()); + println!( + "[1/5] 已读取测试图片:{input_path}({} 字节)", + input_bytes.len() + ); // SegmentCommonImage 要求分辨率低于 2000x2000,超限先等比缩小。 const MAX_EDGE: u32 = 1999; let decoded = image::load_from_memory(&input_bytes).expect("测试图片应可解码"); let input_bytes = if decoded.width() > MAX_EDGE || decoded.height() > MAX_EDGE { - let resized = decoded.resize( - MAX_EDGE, - MAX_EDGE, - image::imageops::FilterType::CatmullRom, - ); + let resized = decoded.resize(MAX_EDGE, MAX_EDGE, image::imageops::FilterType::CatmullRom); let mut buffer = std::io::Cursor::new(Vec::new()); resized .write_to(&mut buffer, image::ImageFormat::Png) @@ -70,8 +69,14 @@ async fn main() { // --- 调用通用抠图 --- // key 优先级:VIAPI 专用 → 官方 SDK 标准命名(#IMAGE_CALL)→ 短信 key 兜底。 let (matting_key_id, matting_key_secret) = [ - ("ALIYUN_IMAGESEG_ACCESS_KEY_ID", "ALIYUN_IMAGESEG_ACCESS_KEY_SECRET"), - ("ALIBABA_CLOUD_ACCESS_KEY_ID", "ALIBABA_CLOUD_ACCESS_KEY_SECRET"), + ( + "ALIYUN_IMAGESEG_ACCESS_KEY_ID", + "ALIYUN_IMAGESEG_ACCESS_KEY_SECRET", + ), + ( + "ALIBABA_CLOUD_ACCESS_KEY_ID", + "ALIBABA_CLOUD_ACCESS_KEY_SECRET", + ), ("ALIYUN_SMS_ACCESS_KEY_ID", "ALIYUN_SMS_ACCESS_KEY_SECRET"), ] .iter() diff --git a/server-rs/crates/platform-matting/src/lib.rs b/server-rs/crates/platform-matting/src/lib.rs index 0df252889..854fafb19 100644 --- a/server-rs/crates/platform-matting/src/lib.rs +++ b/server-rs/crates/platform-matting/src/lib.rs @@ -162,9 +162,9 @@ pub struct UpstreamFailure { impl MattingError { pub fn message(&self) -> &str { match self { - Self::InvalidConfig(message) - | Self::InvalidRequest(message) - | Self::Sign(message) => message, + Self::InvalidConfig(message) | Self::InvalidRequest(message) | Self::Sign(message) => { + message + } Self::Upstream(failure) => &failure.message, } } @@ -270,7 +270,10 @@ impl MattingClient { } let mut form = BTreeMap::new(); - form.insert("Action".to_string(), SEGMENT_COMMON_IMAGE_ACTION.to_string()); + form.insert( + "Action".to_string(), + SEGMENT_COMMON_IMAGE_ACTION.to_string(), + ); form.insert("Format".to_string(), "json".to_string()); form.insert("Version".to_string(), IMAGESEG_API_VERSION.to_string()); form.insert("ImageURL".to_string(), image_url); @@ -451,17 +454,12 @@ impl MattingClient { } async fn download_result_image(&self, url: &str) -> Result, MattingError> { - let mut response = self - .client - .get(url) - .send() - .await - .map_err(|error| { - MattingError::upstream_transport_error( - describe_result_download_transport_error(&error), - error.is_timeout(), - ) - })?; + let mut response = self.client.get(url).send().await.map_err(|error| { + MattingError::upstream_transport_error( + describe_result_download_transport_error(&error), + error.is_timeout(), + ) + })?; let status = response.status(); if !status.is_success() { return Err(MattingError::upstream_http_error( @@ -501,9 +499,7 @@ impl MattingClient { content_type: &str, ) -> Result { if bytes.is_empty() { - return Err(MattingError::InvalidRequest( - "上传内容不能为空".to_string(), - )); + return Err(MattingError::InvalidRequest("上传内容不能为空".to_string())); } let sts = self.get_oss_sts_token().await?; let file_name = file_name.trim().trim_matches('/'); @@ -536,7 +532,8 @@ impl MattingClient { "PUT\n\n{content_type}\n{date}\nx-oss-security-token:{}\n/{VIAPI_TEMP_BUCKET}/{object_key}", sts.security_token ); - let signature = hmac_sha1_base64(sts.access_key_secret.as_bytes(), string_to_sign.as_bytes())?; + let signature = + hmac_sha1_base64(sts.access_key_secret.as_bytes(), string_to_sign.as_bytes())?; let authorization = format!("OSS {}:{}", sts.access_key_id, signature); let target_url = format!("https://{VIAPI_TEMP_OSS_HOST}/{object_key}"); @@ -625,7 +622,9 @@ impl MattingClient { format!( "GetOssStsToken 返回失败(HTTP {},Code={}):{}", http_status.as_u16(), - body.get("Code").and_then(|value| value.as_str()).unwrap_or("unknown"), + body.get("Code") + .and_then(|value| value.as_str()) + .unwrap_or("unknown"), body.get("Message") .and_then(|value| value.as_str()) .unwrap_or("unknown") @@ -762,10 +761,7 @@ fn encode_rgba_png(image: &image::RgbaImage) -> Result, MattingError> { image::ExtendedColorType::Rgba8, ) .map_err(|error| { - MattingError::upstream_response_error( - format!("编码抠图结果 PNG 失败:{error}"), - None, - ) + MattingError::upstream_response_error(format!("编码抠图结果 PNG 失败:{error}"), None) })?; Ok(encoded) } @@ -934,13 +930,7 @@ fn current_aliyun_timestamp() -> String { fn canonicalize_aliyun_form_params(params: &BTreeMap) -> String { params .iter() - .map(|(key, value)| { - format!( - "{}={}", - urlencoding_encode(key), - urlencoding_encode(value) - ) - }) + .map(|(key, value)| format!("{}={}", urlencoding_encode(key), urlencoding_encode(value))) .collect::>() .join("&") } @@ -1001,8 +991,10 @@ mod tests { #[test] fn upstream_transport_error_classifies_as_transport() { - let error = - MattingError::upstream_transport_error("通用抠图请求失败:dns error".to_string(), false); + let error = MattingError::upstream_transport_error( + "通用抠图请求失败:dns error".to_string(), + false, + ); assert!(error.external_call_attempted()); assert!(error.is_transport()); assert!(!error.is_timeout()); @@ -1032,10 +1024,7 @@ mod tests { #[test] fn upstream_response_error_is_external_but_not_transport() { - let error = MattingError::upstream_response_error( - "抠图结果尺寸不一致".to_string(), - None, - ); + let error = MattingError::upstream_response_error("抠图结果尺寸不一致".to_string(), None); assert!(error.external_call_attempted()); assert!(!error.is_transport()); assert!(!error.is_timeout()); @@ -1130,13 +1119,31 @@ mod tests { let sanitized = sanitize_oss_upload_error_body(&body, token); // 敏感串全部消失:StringToSign 明文、其中的 token、十六进制、签名串、以及 Message 里的 token 明文。 - assert!(!sanitized.contains(token), "STS token 不能残留(含元素外的明文)"); - assert!(!sanitized.contains("x-oss-security-token:CAIS"), "StringToSign 明文不能残留"); - assert!(!sanitized.contains("sigSECRET"), "SignatureProvided 不能残留"); - assert!(!sanitized.contains("50 55 54 0a"), "StringToSignBytes 不能残留"); + assert!( + !sanitized.contains(token), + "STS token 不能残留(含元素外的明文)" + ); + assert!( + !sanitized.contains("x-oss-security-token:CAIS"), + "StringToSign 明文不能残留" + ); + assert!( + !sanitized.contains("sigSECRET"), + "SignatureProvided 不能残留" + ); + assert!( + !sanitized.contains("50 55 54 0a"), + "StringToSignBytes 不能残留" + ); // 可诊断信息保留。 - assert!(sanitized.contains("SignatureDoesNotMatch"), "OSS Code 应保留供诊断"); - assert!(sanitized.contains("[redacted]"), "签名材料元素应被脱敏为 [redacted]"); + assert!( + sanitized.contains("SignatureDoesNotMatch"), + "OSS Code 应保留供诊断" + ); + assert!( + sanitized.contains("[redacted]"), + "签名材料元素应被脱敏为 [redacted]" + ); } fn noise_image(width: u32, height: u32) -> image::DynamicImage { @@ -1151,10 +1158,12 @@ mod tests { #[test] fn normalize_keeps_small_image_and_outputs_png() { - let source = - image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(200, 150, image::Rgba([10, 20, 30, 255]))); - let (bytes, dims) = - normalize_matting_input_png(&source).expect("normalize should succeed"); + let source = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel( + 200, + 150, + image::Rgba([10, 20, 30, 255]), + )); + let (bytes, dims) = normalize_matting_input_png(&source).expect("normalize should succeed"); assert_eq!(dims, (200, 150), "小图不缩放,尺寸原样"); assert_eq!( @@ -1168,12 +1177,18 @@ mod tests { #[test] fn normalize_caps_oversized_edge_to_1999() { - let source = - image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(2400, 1200, image::Rgba([0, 0, 0, 255]))); + let source = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel( + 2400, + 1200, + image::Rgba([0, 0, 0, 255]), + )); let (_bytes, (width, height)) = normalize_matting_input_png(&source).expect("normalize should succeed"); - assert!(width <= MAX_INPUT_EDGE && height <= MAX_INPUT_EDGE, "两边都 ≤1999,实得 {width}x{height}"); + assert!( + width <= MAX_INPUT_EDGE && height <= MAX_INPUT_EDGE, + "两边都 ≤1999,实得 {width}x{height}" + ); assert_eq!(width.max(height), MAX_INPUT_EDGE, "最长边压到 1999"); } @@ -1189,7 +1204,10 @@ mod tests { let (bytes, (width, height)) = normalize_matting_input_png_within(&source, limit) .expect("limited normalize should succeed"); - assert!(width < 300 && height < 300, "应从 300x300 降尺寸,实得 {width}x{height}"); + assert!( + width < 300 && height < 300, + "应从 300x300 降尺寸,实得 {width}x{height}" + ); assert!( bytes.len() <= limit || width.min(height) <= MIN_INPUT_EDGE + 1, "编码 {} 字节应落在 {limit} 内(或已触最小边下限)", diff --git a/server-rs/crates/spacetime-client/src/profile_recharge_expiration.rs b/server-rs/crates/spacetime-client/src/profile_recharge_expiration.rs index 7bab87ddc..593f634f2 100644 --- a/server-rs/crates/spacetime-client/src/profile_recharge_expiration.rs +++ b/server-rs/crates/spacetime-client/src/profile_recharge_expiration.rs @@ -67,12 +67,10 @@ impl SpacetimeClient { send_connect_once(&connect_sender, Ok(())); }) .on_disconnect(move |_, error| { - let message = error - .map(|error| error.to_string()) - .unwrap_or_else(|| { - "SpacetimeDB profile recharge expiration subscription disconnected" - .to_string() - }); + let message = error.map(|error| error.to_string()).unwrap_or_else(|| { + "SpacetimeDB profile recharge expiration subscription disconnected" + .to_string() + }); send_connect_once( &disconnect_sender, Err(SpacetimeClientError::Procedure(message)),