From c86ac0422038ba25be5b9f744840d574a6be7985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 19 Sep 2026 13:26:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=20image=5Fprovider=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image_provider/mod.rs 重新声明 #[cfg(test)] mod tests,tests.rs 去掉失效的模块包裹并补齐 helper 导入 提取器 extract_image_urls / extract_b64_images 放宽到 pub(crate),parse_reference_image_data_url 调用补 provider 参数 补回 5 个此前被静默跳过的单元测试 --- .../platform-image/src/image_provider/mod.rs | 3 + .../src/image_provider/protocol/payload.rs | 4 +- .../src/image_provider/tests.rs | 438 +++++++++--------- 3 files changed, 224 insertions(+), 221 deletions(-) diff --git a/server-rs/crates/platform-image/src/image_provider/mod.rs b/server-rs/crates/platform-image/src/image_provider/mod.rs index 9c3b4f2ba..118e9af4d 100644 --- a/server-rs/crates/platform-image/src/image_provider/mod.rs +++ b/server-rs/crates/platform-image/src/image_provider/mod.rs @@ -3,6 +3,9 @@ pub(crate) mod protocol; pub(crate) mod runtime; pub(crate) mod transport; +#[cfg(test)] +mod tests; + pub(crate) use protocol::{payload, request, response}; pub(crate) use runtime::{audit, budget, error, image_source, types, util}; pub(crate) use transport::curl as curl_transport; diff --git a/server-rs/crates/platform-image/src/image_provider/protocol/payload.rs b/server-rs/crates/platform-image/src/image_provider/protocol/payload.rs index b6e420819..1fb999319 100644 --- a/server-rs/crates/platform-image/src/image_provider/protocol/payload.rs +++ b/server-rs/crates/platform-image/src/image_provider/protocol/payload.rs @@ -72,7 +72,7 @@ pub(super) fn extract_generation_id(payload: &Value) -> Option { .or_else(|| find_first_string_by_key(payload, "request_id")) } -pub(super) fn extract_image_urls(payload: &Value) -> Vec { +pub(crate) fn extract_image_urls(payload: &Value) -> Vec { let mut urls = Vec::new(); collect_strings_by_key(payload, "url", &mut urls); collect_strings_by_key(payload, "image", &mut urls); @@ -86,7 +86,7 @@ pub(super) fn extract_image_urls(payload: &Value) -> Vec { deduped } -pub(super) fn extract_b64_images(payload: &Value) -> Vec { +pub(crate) fn extract_b64_images(payload: &Value) -> Vec { let mut values = Vec::new(); collect_strings_by_key(payload, "b64_json", &mut values); collect_inline_image_data(payload, &mut values); diff --git a/server-rs/crates/platform-image/src/image_provider/tests.rs b/server-rs/crates/platform-image/src/image_provider/tests.rs index 3d0acb0c6..692e43a7e 100644 --- a/server-rs/crates/platform-image/src/image_provider/tests.rs +++ b/server-rs/crates/platform-image/src/image_provider/tests.rs @@ -1,223 +1,223 @@ -#[cfg(test)] -mod tests { - use super::*; - use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; - use serde_json::json; +use super::*; +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use serde_json::json; - #[test] - fn request_body_normalizes_size_prompt_and_candidate_count() { - let body = build_image_request_body( - " 风雨夜里的街道 ", - Some(" 低清,水印 "), - " 1:1 ", - 10, - &["data:image/png;base64,AAAA".to_string()], - ); +use super::image_source::{decode_generated_image_base64, parse_reference_image_data_url}; +use super::payload::{extract_b64_images, extract_image_urls}; - assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL); - assert_eq!(body["size"], "1024x1024"); - assert_eq!(body["n"], 4); - assert_eq!(body["prompt"], "风雨夜里的街道\n避免:低清,水印"); - assert!(body.get("image").is_none()); - } +#[test] +fn request_body_normalizes_size_prompt_and_candidate_count() { + let body = build_image_request_body( + " 风雨夜里的街道 ", + Some(" 低清,水印 "), + " 1:1 ", + 10, + &["data:image/png;base64,AAAA".to_string()], + ); - #[test] - fn provider_urls_normalize_root_and_v1_base_urls() { - let root_settings = ImageProviderSettings { - provider: ImageProvider::VectorEngine, - base_url: "https://vector.example".to_string(), - api_key: "test-key".to_string(), - request_timeout_ms: 1_000, - request_deadline: None, - }; - let v1_settings = ImageProviderSettings { - provider: ImageProvider::VectorEngine, - base_url: "https://vector.example/v1".to_string(), - api_key: "test-key".to_string(), - request_timeout_ms: 1_000, - request_deadline: None, - }; - - assert_eq!( - images_generation_url(&root_settings), - "https://vector.example/v1/images/generations" - ); - assert_eq!( - images_generation_url(&v1_settings), - "https://vector.example/v1/images/generations" - ); - assert_eq!( - images_edit_url(&root_settings), - "https://vector.example/v1/images/edits" - ); - assert_eq!( - images_edit_url(&v1_settings), - "https://vector.example/v1/images/edits" - ); - } - - #[test] - fn data_url_and_base64_image_decoding_preserves_image_metadata() { - let data_url = format!( - "data:image/png;base64,{}", - BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest") - ); - - let reference = parse_reference_image_data_url(&data_url, 2) - .expect("data url should parse") - .expect("image data url should be accepted"); - assert_eq!(reference.file_name, "reference-2.png"); - assert_eq!(reference.mime_type, "image/png"); - assert_eq!(reference.bytes, b"\x89PNG\r\n\x1A\nrest"); - - let image = decode_generated_image_base64( - BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest").as_str(), - ) - .expect("base64 image should decode"); - assert_eq!(image.extension, "png"); - assert_eq!(image.mime_type, "image/png"); - assert_eq!(image.bytes, b"\x89PNG\r\n\x1A\nrest"); - } - - #[test] - fn error_status_hints_and_audit_fields_are_structured() { - let audit = PlatformImageFailureAudit { - provider: VECTOR_ENGINE_PROVIDER, - endpoint: "https://vector.example/v1/images/generations".to_string(), - operation: "图片生成失败".to_string(), - failure_stage: "upstream_status", - status_code: Some(504), - status_class: Some("5xx"), - timeout: true, - retryable: true, - error_message: "上游超时".to_string(), - error_source: Some("read timeout".to_string()), - raw_excerpt: Some("{\"error\":\"timeout\"}".to_string()), - latency_ms: Some(987), - prompt_chars: Some(64), - reference_image_count: Some(2), - image_model: Some(GPT_IMAGE_2_MODEL), - }; - - let request_error = PlatformImageError::Request { - provider: VECTOR_ENGINE_PROVIDER, - message: "请求发送失败".to_string(), - endpoint: Some("https://vector.example/v1/images/generations".to_string()), - timeout: true, - connect: false, - request: true, - body: false, - status_code: None, - source: None, - audit: None, - }; - let invalid_config = PlatformImageError::InvalidConfig { - provider: VECTOR_ENGINE_PROVIDER, - message: "缺少配置".to_string(), - }; - let invalid_request = PlatformImageError::InvalidRequest { - provider: VECTOR_ENGINE_PROVIDER, - message: "请求不合法".to_string(), - }; - let upstream_timeout = PlatformImageError::Upstream { - provider: VECTOR_ENGINE_PROVIDER, - message: "upstream timeout".to_string(), - upstream_status: 502, - raw_excerpt: "deadline has elapsed".to_string(), - audit: Some(audit.clone()), - }; - - assert_eq!( - invalid_config.status_hint(), - PlatformImageStatusHint::ServiceUnavailable - ); - assert_eq!( - invalid_request.status_hint(), - PlatformImageStatusHint::BadRequest - ); - assert_eq!( - request_error.status_hint(), - PlatformImageStatusHint::GatewayTimeout - ); - assert_eq!( - upstream_timeout.status_hint(), - PlatformImageStatusHint::GatewayTimeout - ); - assert_eq!( - PlatformImageError::MissingImage { - provider: VECTOR_ENGINE_PROVIDER, - message: "缺图".to_string(), - audit: Some(audit.clone()), - } - .status_hint(), - PlatformImageStatusHint::BadGateway - ); - - let audit_ref = upstream_timeout.audit().expect("audit should be preserved"); - assert_eq!(audit_ref.provider, VECTOR_ENGINE_PROVIDER); - assert_eq!( - audit_ref.endpoint, - "https://vector.example/v1/images/generations" - ); - assert_eq!(audit_ref.status_code, Some(504)); - assert_eq!(audit_ref.status_class, Some("5xx")); - assert!(audit_ref.timeout); - assert!(audit_ref.retryable); - assert_eq!(audit_ref.reference_image_count, Some(2)); - assert_eq!(audit_ref.image_model, Some(GPT_IMAGE_2_MODEL)); - assert!(invalid_config.audit().is_none()); - assert!(invalid_request.audit().is_none()); - } - - #[test] - fn extract_image_urls_and_b64_values_are_deduped() { - let payload = json!({ - "data": [ - {"image": "https://example.com/a.png"}, - {"url": "https://example.com/a.png"}, - {"image_url": "ftp://example.com/b.png"}, - {"url": "https://example.com/b.png"} - ], - "nested": { - "b64_json": ["YWJj", "ZGVm"], - "parts": [ - { - "inlineData": { - "mimeType": "image/png", - "data": "aW1hZ2UtMQ==" - } - }, - { - "inline_data": { - "mime_type": "image/jpeg", - "data": "aW1hZ2UtMg==" - } - }, - { - "inlineData": { - "mimeType": "text/plain", - "data": "bm90LWltYWdl" - } - } - ] - } - }); - - assert_eq!( - extract_image_urls(&payload), - vec![ - "https://example.com/a.png".to_string(), - "https://example.com/b.png".to_string() - ] - ); - assert_eq!( - extract_b64_images(&payload), - vec![ - "YWJj".to_string(), - "ZGVm".to_string(), - "aW1hZ2UtMQ==".to_string(), - "aW1hZ2UtMg==".to_string(), - ] - ); - } + assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL); + assert_eq!(body["size"], "1024x1024"); + assert_eq!(body["n"], 4); + assert_eq!(body["prompt"], "风雨夜里的街道\n避免:低清,水印"); + assert!(body.get("image").is_none()); +} + +#[test] +fn provider_urls_normalize_root_and_v1_base_urls() { + let root_settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, + base_url: "https://vector.example".to_string(), + api_key: "test-key".to_string(), + request_timeout_ms: 1_000, + request_deadline: None, + }; + let v1_settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, + base_url: "https://vector.example/v1".to_string(), + api_key: "test-key".to_string(), + request_timeout_ms: 1_000, + request_deadline: None, + }; + + assert_eq!( + images_generation_url(&root_settings), + "https://vector.example/v1/images/generations" + ); + assert_eq!( + images_generation_url(&v1_settings), + "https://vector.example/v1/images/generations" + ); + assert_eq!( + images_edit_url(&root_settings), + "https://vector.example/v1/images/edits" + ); + assert_eq!( + images_edit_url(&v1_settings), + "https://vector.example/v1/images/edits" + ); +} + +#[test] +fn data_url_and_base64_image_decoding_preserves_image_metadata() { + let data_url = format!( + "data:image/png;base64,{}", + BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest") + ); + + let reference = parse_reference_image_data_url(&data_url, 2, ImageProvider::VectorEngine) + .expect("data url should parse") + .expect("image data url should be accepted"); + assert_eq!(reference.file_name, "reference-2.png"); + assert_eq!(reference.mime_type, "image/png"); + assert_eq!(reference.bytes, b"\x89PNG\r\n\x1A\nrest"); + + let image = + decode_generated_image_base64(BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest").as_str()) + .expect("base64 image should decode"); + assert_eq!(image.extension, "png"); + assert_eq!(image.mime_type, "image/png"); + assert_eq!(image.bytes, b"\x89PNG\r\n\x1A\nrest"); +} + +#[test] +fn error_status_hints_and_audit_fields_are_structured() { + let audit = PlatformImageFailureAudit { + provider: VECTOR_ENGINE_PROVIDER, + endpoint: "https://vector.example/v1/images/generations".to_string(), + operation: "图片生成失败".to_string(), + failure_stage: "upstream_status", + status_code: Some(504), + status_class: Some("5xx"), + timeout: true, + retryable: true, + error_message: "上游超时".to_string(), + error_source: Some("read timeout".to_string()), + raw_excerpt: Some("{\"error\":\"timeout\"}".to_string()), + latency_ms: Some(987), + prompt_chars: Some(64), + reference_image_count: Some(2), + image_model: Some(GPT_IMAGE_2_MODEL), + }; + + let request_error = PlatformImageError::Request { + provider: VECTOR_ENGINE_PROVIDER, + message: "请求发送失败".to_string(), + endpoint: Some("https://vector.example/v1/images/generations".to_string()), + timeout: true, + connect: false, + request: true, + body: false, + status_code: None, + source: None, + audit: None, + }; + let invalid_config = PlatformImageError::InvalidConfig { + provider: VECTOR_ENGINE_PROVIDER, + message: "缺少配置".to_string(), + }; + let invalid_request = PlatformImageError::InvalidRequest { + provider: VECTOR_ENGINE_PROVIDER, + message: "请求不合法".to_string(), + }; + let upstream_timeout = PlatformImageError::Upstream { + provider: VECTOR_ENGINE_PROVIDER, + message: "upstream timeout".to_string(), + upstream_status: 502, + raw_excerpt: "deadline has elapsed".to_string(), + audit: Some(audit.clone()), + }; + + assert_eq!( + invalid_config.status_hint(), + PlatformImageStatusHint::ServiceUnavailable + ); + assert_eq!( + invalid_request.status_hint(), + PlatformImageStatusHint::BadRequest + ); + assert_eq!( + request_error.status_hint(), + PlatformImageStatusHint::GatewayTimeout + ); + assert_eq!( + upstream_timeout.status_hint(), + PlatformImageStatusHint::GatewayTimeout + ); + assert_eq!( + PlatformImageError::MissingImage { + provider: VECTOR_ENGINE_PROVIDER, + message: "缺图".to_string(), + audit: Some(audit.clone()), + } + .status_hint(), + PlatformImageStatusHint::BadGateway + ); + + let audit_ref = upstream_timeout.audit().expect("audit should be preserved"); + assert_eq!(audit_ref.provider, VECTOR_ENGINE_PROVIDER); + assert_eq!( + audit_ref.endpoint, + "https://vector.example/v1/images/generations" + ); + assert_eq!(audit_ref.status_code, Some(504)); + assert_eq!(audit_ref.status_class, Some("5xx")); + assert!(audit_ref.timeout); + assert!(audit_ref.retryable); + assert_eq!(audit_ref.reference_image_count, Some(2)); + assert_eq!(audit_ref.image_model, Some(GPT_IMAGE_2_MODEL)); + assert!(invalid_config.audit().is_none()); + assert!(invalid_request.audit().is_none()); +} + +#[test] +fn extract_image_urls_and_b64_values_are_deduped() { + let payload = json!({ + "data": [ + {"image": "https://example.com/a.png"}, + {"url": "https://example.com/a.png"}, + {"image_url": "ftp://example.com/b.png"}, + {"url": "https://example.com/b.png"} + ], + "nested": { + "b64_json": ["YWJj", "ZGVm"], + "parts": [ + { + "inlineData": { + "mimeType": "image/png", + "data": "aW1hZ2UtMQ==" + } + }, + { + "inline_data": { + "mime_type": "image/jpeg", + "data": "aW1hZ2UtMg==" + } + }, + { + "inlineData": { + "mimeType": "text/plain", + "data": "bm90LWltYWdl" + } + } + ] + } + }); + + assert_eq!( + extract_image_urls(&payload), + vec![ + "https://example.com/a.png".to_string(), + "https://example.com/b.png".to_string() + ] + ); + assert_eq!( + extract_b64_images(&payload), + vec![ + "YWJj".to_string(), + "ZGVm".to_string(), + "aW1hZ2UtMQ==".to_string(), + "aW1hZ2UtMg==".to_string(), + ] + ); }