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 c5dd38c37..4c319b003 100644 --- a/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md +++ b/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md @@ -35,7 +35,7 @@ POST /api/raw/v1/images/edit } ``` -`image` 是必填的单图结构 `{ data, mimeType }`;`mask` 可选并使用相同结构。`image` 和 `mask` 的 `mimeType` 必须为 `image/png`,base64 解码后必须是可完整解码的有效 PNG 文件,提供 mask 时其宽高必须与 image 完全一致;空数据、非 PNG 字节、MIME 不匹配或尺寸不一致均在扣费前返回 400。服务端不把输入格式另建成请求参数。`prompt` 必填。`quality`、`background` 和 `output_format` 采用 GPT Image 模型支持的值。 +`image` 是必填的单图结构 `{ data, mimeType }`;`mask` 可选并使用相同结构。`image` 和 `mask` 的 `mimeType` 必须为 `image/png`,base64 解码后必须是可完整解码的有效 PNG 文件,提供 mask 时其宽高必须与 image 完全一致;空数据、非 PNG 字节、MIME 不匹配或尺寸不一致均在扣费前返回 400。服务端不把输入格式另建成请求参数。`prompt` 必填,UTF-8 原始字节长度不得超过 `16 KiB`;超限在扣费前返回 400。`quality`、`background` 和 `output_format` 采用 GPT Image 模型支持的值。 `width`、`height` 使用严格输出尺寸规则,均在扣费前校验: diff --git a/server-rs/crates/api-server/src/raw_image.rs b/server-rs/crates/api-server/src/raw_image.rs index e0e88a876..1c46e7fbc 100644 --- a/server-rs/crates/api-server/src/raw_image.rs +++ b/server-rs/crates/api-server/src/raw_image.rs @@ -59,6 +59,8 @@ pub(crate) struct RawImageEditResponse { pub(crate) data: Vec, } +const RAW_IMAGE_MAX_PROMPT_BYTES: usize = 16 * 1024; + pub(crate) async fn edit_raw_image( State(state): State, Extension(request_context): Extension, @@ -156,6 +158,11 @@ fn prepare_request(payload: RawImageEditRequest) -> Result RAW_IMAGE_MAX_PROMPT_BYTES { + return Err(bad_request(format!( + "prompt 不能超过 {RAW_IMAGE_MAX_PROMPT_BYTES} 字节" + ))); + } validate_raw_image_edit_dimensions(payload.width, payload.height) .map_err(|error| bad_request(error.to_string()))?; validate_optional_value( @@ -430,4 +437,21 @@ mod tests { assert!(format!("{error:?}").contains("mask 尺寸必须与 image 一致")); } + + #[test] + fn prompt_uses_raw_utf8_byte_limit() { + let payload = serde_json::json!({ + "image": {"data": encoded_png(1, 1), "mimeType": "image/png"}, + "prompt": "a".repeat(RAW_IMAGE_MAX_PROMPT_BYTES + 1), + "width": 1024, + "height": 1024 + }); + let parsed: RawImageEditRequest = serde_json::from_value(payload).expect("request"); + let error = match prepare_request(parsed) { + Ok(_) => panic!("oversized prompt should fail before image decode and billing"), + Err(error) => error, + }; + + assert!(format!("{error:?}").contains("prompt 不能超过 16384 字节")); + } } diff --git a/server-rs/crates/platform-image/Cargo.toml b/server-rs/crates/platform-image/Cargo.toml index 9da088343..2030dab06 100644 --- a/server-rs/crates/platform-image/Cargo.toml +++ b/server-rs/crates/platform-image/Cargo.toml @@ -9,6 +9,7 @@ base64 = { workspace = true } curl = { workspace = true } image = { workspace = true, features = ["jpeg", "png", "webp"] } reqwest = { workspace = true, features = ["json", "multipart", "rustls-tls"] } +serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["io-util", "macros", "net", "time"] } tracing = { workspace = true }