From b3693765ef5693ed1edcafc605b101bd7a200756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 9 Sep 2026 11:34:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=8F=E4=BC=A0=20Raw=20=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E9=81=BF=E5=85=8D=E8=A7=A3=E7=A0=81=E5=9B=9E?= =?UTF-8?q?=E6=98=BE=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅转发上游 data.b64_json 并忽略 output_format 同步 Raw 技术方案并补充不解码回归测试 --- ...案】Raw GPT Image 2图片编辑代理-2026-09-07.md | 4 +- server-rs/crates/api-server/src/raw_image.rs | 6 +- server-rs/crates/platform-image/src/lib.rs | 2 +- .../platform-image/src/vector_engine/mod.rs | 4 +- .../src/vector_engine/raw_edit.rs | 197 +++--------------- .../platform-image/src/vector_engine/types.rs | 7 + 6 files changed, 40 insertions(+), 180 deletions(-) diff --git a/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md b/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md index 9bd8cfd4f..12b1f0331 100644 --- a/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md +++ b/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md @@ -93,12 +93,12 @@ raw 操作使用独立的 operation / ledger 命名空间,例如 `raw-image-ed `platform-image` 保留 VectorEngine 协议细节。raw handler 只负责:认证、JSON DTO、base64 解码、预检查、计费编排和响应映射。provider 请求仍由 `platform-image` 统一构造,并携带 `model`、`n`、`quality`、`background`、`output_format`、尺寸及图片参考字节。 -provider 结果统一解码为图片字节;每项结果的 MIME 与扩展名以 VectorEngine 响应中的真实 `output_format` 为准,不得从请求参数反推;entry 级格式为空或非字符串时回退 payload 级格式。发送、响应读取、上游状态、响应解析和缺图失败必须生成 `PlatformImageFailureAudit`,由 api-server 写入现有外部 API 失败审计链;成功结果同时写入统一的 `external_generation_run` 追踪事件。raw handler 只将结果字节编码到 `data[].b64_json`。 +provider 响应只提取并透传 `data[].b64_json` 字符串,不在服务端解码图片 base64,也不读取或回传 provider 的 `output_format`(该字段只是请求参数回显)。发送、响应读取、上游状态、响应解析和缺图失败必须生成 `PlatformImageFailureAudit`,由 api-server 写入现有外部 API 失败审计链;成功结果同时写入统一的 `external_generation_run` 追踪事件。raw handler 只将上游 `b64_json` 原样写入 `data[].b64_json`。 ## 代码拆分 - `server-rs/crates/api-server/src/raw_image.rs`:独立路由 handler、请求/响应 DTO、base64 输入校验、预检查和 raw billing 编排。 -- `server-rs/crates/platform-image/src/vector_engine/raw_edit.rs`:raw 编辑选项、严格尺寸校验、独立 provider 请求映射和原始响应解码;不复用现有 editor 图片编辑 client 或其 multipart transport。 +- `server-rs/crates/platform-image/src/vector_engine/raw_edit.rs`:raw 编辑选项、严格尺寸校验、独立 provider 请求映射和 `b64_json` 响应透传;不复用现有 editor 图片编辑 client 或其 multipart transport。 - `server-rs/crates/api-server/src/modules/raw.rs`:只注册 `/api/raw/v1/images/edit` 并挂载 Bearer middleware。 不修改 External v1 OpenAPI;不在 `external_editor_api.rs`、编辑器项目模块或外部生成 worker 中增加 raw 分支。 diff --git a/server-rs/crates/api-server/src/raw_image.rs b/server-rs/crates/api-server/src/raw_image.rs index cea19316c..456dcca69 100644 --- a/server-rs/crates/api-server/src/raw_image.rs +++ b/server-rs/crates/api-server/src/raw_image.rs @@ -109,11 +109,9 @@ pub(crate) async fn edit_raw_image( }; let task_id = generated.task_id.clone(); let data: Vec = generated - .images + .b64_images .into_iter() - .map(|image| RawImageEditItem { - b64_json: BASE64_STANDARD.encode(image.bytes), - }) + .map(|b64_json| RawImageEditItem { b64_json }) .collect(); if let Some(state) = tracking_state.as_ref() { record_external_generation_run_after_success( diff --git a/server-rs/crates/platform-image/src/lib.rs b/server-rs/crates/platform-image/src/lib.rs index 7140aec48..c527d4c7d 100644 --- a/server-rs/crates/platform-image/src/lib.rs +++ b/server-rs/crates/platform-image/src/lib.rs @@ -12,7 +12,7 @@ pub use vector_engine::{ DownloadedImage, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, GeneratedImages, NANOBANANA_2_MODEL, PlatformImageError, PlatformImageFailureAudit, PlatformImageStatusHint, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, - RawImageEditDimensionError, RawImageEditOptions, ReferenceImage, + RawImageEditDimensionError, RawImageEditOptions, RawImageEditResult, ReferenceImage, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings, build_vector_engine_image_http_client, build_vector_engine_image_request_body, build_vector_engine_nanobanana_generate_content_request_body, create_vector_engine_image_edit, diff --git a/server-rs/crates/platform-image/src/vector_engine/mod.rs b/server-rs/crates/platform-image/src/vector_engine/mod.rs index b94d22221..351244650 100644 --- a/server-rs/crates/platform-image/src/vector_engine/mod.rs +++ b/server-rs/crates/platform-image/src/vector_engine/mod.rs @@ -38,4 +38,6 @@ pub use request::{ vector_engine_nanobanana_generate_content_url, }; pub use transport::build_vector_engine_image_http_client; -pub use types::{DownloadedImage, GeneratedImages, ReferenceImage, VectorEngineImageSettings}; +pub use types::{ + DownloadedImage, GeneratedImages, RawImageEditResult, ReferenceImage, VectorEngineImageSettings, +}; diff --git a/server-rs/crates/platform-image/src/vector_engine/raw_edit.rs b/server-rs/crates/platform-image/src/vector_engine/raw_edit.rs index 1373d8545..c0d78f1e8 100644 --- a/server-rs/crates/platform-image/src/vector_engine/raw_edit.rs +++ b/server-rs/crates/platform-image/src/vector_engine/raw_edit.rs @@ -1,6 +1,5 @@ use std::time::{Duration, Instant}; -use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use reqwest::multipart::{Form, Part}; use serde_json::Value; @@ -13,7 +12,7 @@ use super::{ }, error::PlatformImageError, request::vector_engine_images_edit_url, - types::{DownloadedImage, GeneratedImages, ReferenceImage, VectorEngineImageSettings}, + types::{RawImageEditResult, ReferenceImage, VectorEngineImageSettings}, util::truncate_raw, }; @@ -98,7 +97,7 @@ pub async fn create_vector_engine_raw_image_edit( image: &ReferenceImage, options: RawImageEditOptions, failure_context: &str, -) -> Result { +) -> Result { validate_raw_image_edit_dimensions(options.width, options.height) .map_err(|error| invalid_input(failure_context, error.to_string()))?; let url = vector_engine_images_edit_url(settings); @@ -250,61 +249,8 @@ pub async fn create_vector_engine_raw_image_edit( }); } }; - let mut images = Vec::new(); - if let Some(entries) = payload.get("data").and_then(Value::as_array) { - for entry in entries { - let Some(value) = entry.get("b64_json").and_then(Value::as_str) else { - continue; - }; - let bytes = match BASE64_STANDARD.decode(value) { - Ok(bytes) => bytes, - Err(error) => { - let message = format!("{failure_context}:上游 b64_json 解码失败:{error}"); - let audit = build_failure_audit( - url.as_str(), - failure_context, - "response_parse", - Some(status.as_u16()), - Some(status_class(status.as_u16())), - false, - false, - message.as_str(), - Some(error.to_string()), - Some(truncate_raw(body.as_str())), - Some(started_at.elapsed().as_millis() as u64), - prompt_chars, - reference_image_count, - Some(GPT_IMAGE_2_MODEL), - ); - return Err(PlatformImageError::ResponseParse { - provider: VECTOR_ENGINE_PROVIDER, - message, - raw_excerpt: truncate_raw(body.as_str()), - audit: Some(audit), - }); - } - }; - let (mime_type, extension) = - response_image_format(&payload, entry).map_err(|message| { - response_parse_error( - &url, - failure_context, - message, - status.as_u16(), - started_at, - prompt_chars, - reference_image_count, - &body, - ) - })?; - images.push(DownloadedImage { - bytes, - mime_type: mime_type.to_string(), - extension: extension.to_string(), - }); - } - } - if images.is_empty() { + let b64_images = extract_b64_images(&payload); + if b64_images.is_empty() { let message = format!("{failure_context}:上游未返回 b64_json 图片"); let audit = build_failure_audit( url.as_str(), @@ -328,18 +274,28 @@ pub async fn create_vector_engine_raw_image_edit( audit: Some(audit), }); } - Ok(GeneratedImages { + Ok(RawImageEditResult { task_id: payload .get("id") .and_then(Value::as_str) .unwrap_or("raw-image-edit") .to_string(), - actual_prompt: None, - images, + b64_images, recovered_failure_audits: Vec::new(), }) } +fn extract_b64_images(payload: &Value) -> Vec { + payload + .get("data") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|entry| entry.get("b64_json").and_then(Value::as_str)) + .map(str::to_string) + .collect() +} + fn invalid_request(context: &str, message: String) -> PlatformImageError { PlatformImageError::InvalidRequest { provider: VECTOR_ENGINE_PROVIDER, @@ -361,68 +317,6 @@ fn invalid_config(context: &str, message: String) -> PlatformImageError { } } -fn response_parse_error( - url: &str, - context: &str, - message: &str, - status: u16, - started_at: Instant, - prompt_chars: Option, - reference_image_count: Option, - body: &str, -) -> PlatformImageError { - let audit = build_failure_audit( - url, - context, - "response_parse", - Some(status), - Some(status_class(status)), - false, - false, - message, - None, - Some(truncate_raw(body)), - Some(started_at.elapsed().as_millis() as u64), - prompt_chars, - reference_image_count, - Some(GPT_IMAGE_2_MODEL), - ); - PlatformImageError::ResponseParse { - provider: VECTOR_ENGINE_PROVIDER, - message: format!("{context}:{message}"), - raw_excerpt: truncate_raw(body), - audit: Some(audit), - } -} - -fn response_image_format( - payload: &Value, - entry: &Value, -) -> Result<(&'static str, &'static str), &'static str> { - let Some(value) = entry - .get("output_format") - .and_then(Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - .or_else(|| { - payload - .get("output_format") - .and_then(Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - }) - else { - return Err("上游响应缺少 output_format"); - }; - match value.to_ascii_lowercase().as_str() { - "png" => Ok(("image/png", "png")), - "jpeg" | "jpg" => Ok(("image/jpeg", "jpg")), - "webp" => Ok(("image/webp", "webp")), - "gif" => Ok(("image/gif", "gif")), - _ => Err("上游响应包含不支持的 output_format"), - } -} - fn request_error( url: &str, context: &str, @@ -495,57 +389,16 @@ mod tests { use serde_json::json; #[test] - fn response_format_uses_vector_engine_output_format() { - let payload = json!({"output_format": "png"}); - assert_eq!( - response_image_format(&payload, &json!({"output_format": "webp"})), - Ok(("image/webp", "webp")) - ); - assert_eq!( - response_image_format(&payload, &json!({})), - Ok(("image/png", "png")) - ); - assert_eq!( - response_image_format(&json!({}), &json!({"output_format": "jpeg"})), - Ok(("image/jpeg", "jpg")) - ); - assert_eq!( - response_image_format( - &json!({"output_format": "png"}), - &json!({"output_format": ""}) - ), - Ok(("image/png", "png")) - ); - assert_eq!( - response_image_format( - &json!({"output_format": "webp"}), - &json!({"output_format": null}) - ), - Ok(("image/webp", "webp")) - ); - assert!(response_image_format(&json!({}), &json!({})).is_err()); - assert!(response_image_format(&json!({}), &json!({"output_format": "bmp"})).is_err()); - } + fn raw_result_forwards_b64_without_decoding_or_using_output_format() { + let payload = json!({ + "output_format": "png", + "data": [{"b64_json": "not-base64-but-forwarded", "output_format": "jpeg"}] + }); - #[test] - fn response_parse_error_contains_structured_audit() { - let error = response_parse_error( - "https://vector.example/v1/images/edits", - "raw_image_edit", - "上游响应缺少 output_format", - 200, - Instant::now(), - Some(12), - Some(2), - "{\"data\":[]}", + assert_eq!( + extract_b64_images(&payload), + vec!["not-base64-but-forwarded".to_string()] ); - let audit = error.audit().expect("response error should carry audit"); - assert_eq!(audit.failure_stage, "response_parse"); - assert_eq!(audit.status_code, Some(200)); - assert_eq!(audit.status_class, Some("2xx")); - assert_eq!(audit.prompt_chars, Some(12)); - assert_eq!(audit.reference_image_count, Some(2)); - assert_eq!(audit.image_model, Some(GPT_IMAGE_2_MODEL)); } #[test] diff --git a/server-rs/crates/platform-image/src/vector_engine/types.rs b/server-rs/crates/platform-image/src/vector_engine/types.rs index 77fbd19f9..82a69bb4a 100644 --- a/server-rs/crates/platform-image/src/vector_engine/types.rs +++ b/server-rs/crates/platform-image/src/vector_engine/types.rs @@ -16,6 +16,13 @@ pub struct GeneratedImages { pub recovered_failure_audits: Vec, } +#[derive(Clone, Debug)] +pub struct RawImageEditResult { + pub task_id: String, + pub b64_images: Vec, + pub recovered_failure_audits: Vec, +} + #[derive(Clone, Debug)] pub struct DownloadedImage { pub bytes: Vec,