整理图片 provider 模块层次并拆出 raw edit
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m26s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m39s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m0s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m37s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m28s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m26s
Project CI / Frontend tests (pull_request) Failing after 5m49s
Project CI / Native shell tests (pull_request) Successful in 10m45s

将 raw image edit 提取到独立 raw_image_edit 模块

按 protocol、runtime、transport 目录收拢 image_provider 实现

同步后端架构与 raw edit 技术文档路径
This commit is contained in:
2026-09-18 17:45:09 +08:00
parent 1367e9c04e
commit 1574f206a2
20 changed files with 102 additions and 71 deletions
@@ -90,7 +90,7 @@ provider 响应只提取并透传 `data[].b64_json` 字符串,不在服务端
## 代码拆分
- `server-rs/crates/api-server/src/raw_image.rs`:独立路由 handler、multipart 字段解析、请求/响应 DTO、PNG 输入校验、预检查和 raw billing 编排。
- `server-rs/crates/platform-image/src/vector_engine/raw_edit.rs`:raw 编辑选项、严格尺寸校验、独立 provider 请求映射和 `b64_json` 响应透传;由 api-server 按现有图片 API 传统构造并传入共享的 VectorEngine `reqwest::Client`,不在每个 raw 调用内部重复构造 client。每次请求仍用 `effective_request_timeout_ms` 通过 request builder 设置剩余 deadline;不修改统一 transport builder 的连接池策略。
- `server-rs/crates/platform-image/src/raw_image_edit/mod.rs`:raw 编辑选项、严格尺寸校验、独立 provider 请求映射和 `b64_json` 响应透传;由 api-server 按现有图片 API 传统构造并传入启动时构造的 Tiantoken `reqwest::Client`,不在每个 raw 调用内部重复构造 client。每次请求仍用 `effective_request_timeout_ms` 通过 request builder 设置剩余 deadline;不修改统一 transport builder 的连接池策略。
raw-edit 的图片输入使用独立的 `RawImageEditImage`(`bytes::Bytes`),由 reqwest `Part::stream(Body::from(Bytes))` 直接接管 multipart 请求体,避免整图和掩码在 `Part::bytes` 的 `Cow<[u8]>` 转换中再次复制。既有 `ReferenceImage`、`DownloadedImage` 及 curl/编辑器链路继续保持 `Vec<u8>` 契约,不因 raw-edit 引入全局字节类型迁移。
@@ -168,7 +168,7 @@ npm run check:server-rs-ddd
2. Adapter 输入应显式包含 provider、prompt、reference images、OSS prefix/path/file name、asset kind、entity kind/id、slot、owner/profile/source job、metadata 和可选透明背景后处理。
3. Adapter 输出应保留 legacy public path、object key、asset object id、MIME、extension、task id 和实际 prompt。
4. Adapter 不负责扣费、退款或钱包读取;计费仍由调用方显式包裹。
5. 图片 provider 协议不再放在玩法模块里实现。产品、计费、DTO、持久化和 VectorEngine 创建 / 编辑首选请求统一使用 `gpt-image-2`;只有符合回退条件时,provider 边界才切到兜底模型 `gpt-image-2-c`。URL / base64 图片解析、远端图片下载、请求超时 / 上游状态 / 响应解析 / 缺图 / 下载失败的结构化日志统一在 `server-rs/crates/platform-image/src/vector_engine/`;其中 `client.rs` 只保留 provider 调用编排,`transport.rs` 负责 HTTP client 与 reqwest 错误归一,`request.rs` 负责请求体和路径,`payload.rs` 负责响应 JSON 字段提取,`response.rs` 负责响应状态分流和图片结果归一。`api-server` 只负责配置校验、玩法 prompt 编排、OSS / asset object / binding 持久化、计费和外部 API 失败审计落库。
5. 图片 provider 协议不再放在玩法模块里实现。产品、计费、DTO、持久化和 GPT Image 2.5 创建 / 编辑请求统一使用业务模型 `gpt-image-2.5`,api-server 在提交边界分派 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c`;已持久化的 `gpt-image-2` / `gpt-image-2-c` 只按兼容规则读取,不改写历史值。URL / base64 图片解析、远端图片下载、请求超时 / 上游状态 / 响应解析 / 缺图 / 下载失败的结构化日志统一在 `server-rs/crates/platform-image/src/image_provider/`;其中 `runtime/executor.rs` 负责共享 provider-neutral 执行编排,`transport/` 负责 HTTP client 与 curl 传输,`protocol/` 负责请求体、路径和响应 JSON 字段。raw image edit 的 multipart 与严格尺寸校验位于独立的 `server-rs/crates/platform-image/src/raw_image_edit/`。`api-server` 只负责配置校验、玩法 prompt 编排、OSS / asset object / binding 持久化、计费和外部 API 失败审计落库。
6. OSS 平台适配日志统一在 `server-rs/crates/platform-oss` 输出,覆盖 `sign_post_object`、`sign_get_object_url`、`head_object` 和 `put_object`。日志字段固定使用 `provider`、`operation`、`bucket`、`endpoint`、`object_key` / `key_prefix`、`access`、`content_type`、`content_length`、`status`、`status_class`、`error_kind` 和 `elapsed_ms`,只记录对象定位和排障信息;不得输出 AccessKey、policy、signature、Authorization header 或完整 signed URL。generated 私有对象上传时必须由 OSS 对象头承载浏览器 / CDN 缓存策略,默认写入 `Cache-Control: public, max-age=31536000, immutable`,不得改成 api-server 本地磁盘静态资源兜底。
7. Puzzle、Match3D、音频、GLB、视频等复杂媒体可以复用 OSS + asset object + binding 的底层持久化能力,但玩法专属处理规则留在各自编排层,不塞进公共接口。
8. 拼图入口页与结果页新增关卡的本地参考图不走浏览器直传 OSS,前端读取为 Data URL 后随创作 action 提交,并在读取前限制 6MB、显示“图片≤6MB”。`api-server` 必须对 Data URL 实际字节数再次校验;历史图片才提交 `referenceImageAssetObjectId(s)`,后端校验 `asset_object` 的 bucket、kind、图片 MIME、大小和 owner 后签发只读 URL 给 VectorEngine 读取。
@@ -1,44 +1,27 @@
mod audit;
mod budget;
mod client;
mod constants;
mod curl_transport;
mod error;
mod image_source;
mod payload;
mod raw_edit;
mod request;
mod response;
mod transport;
mod types;
mod util;
pub(crate) mod constants;
pub(crate) mod protocol;
pub(crate) mod runtime;
pub(crate) mod transport;
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;
pub use audit::PlatformImageFailureAudit;
pub use client::{
create_image_edit, create_image_edit_with_references,
create_image_edit_with_references_and_model, create_image_generation,
create_image_generation_with_model, create_nanobanana_generate_content,
};
pub use constants::TIANTOKEN_PROVIDER;
pub use constants::{
GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL,
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, VECTOR_ENGINE_PROVIDER,
};
pub use error::{PlatformImageError, PlatformImageStatusHint};
pub use image_source::download_remote_image;
pub use raw_edit::{
GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE,
RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditImage,
RawImageEditOptions, RawImageEditResult, create_raw_image_edit,
validate_raw_image_edit_dimensions,
};
pub use request::{
pub use protocol::{
build_image_request_body, build_image_request_body_with_model,
build_nanobanana_generate_content_request_body, images_edit_url, images_generation_url,
nanobanana_generate_content_url, normalize_image_size_for_model, resolve_image_provider,
};
pub use transport::{build_image_http_client, build_image_provider_client};
pub use types::{
pub use runtime::{
DownloadedImage, GeneratedImages, ImageProvider, ImageProviderClient, ImageProviderSettings,
ReferenceImage,
PlatformImageError, PlatformImageFailureAudit, PlatformImageStatusHint, ReferenceImage,
create_image_edit, create_image_edit_with_references,
create_image_edit_with_references_and_model, create_image_generation,
create_image_generation_with_model, create_nanobanana_generate_content, download_remote_image,
};
pub use transport::{build_image_http_client, build_image_provider_client};
@@ -0,0 +1,12 @@
#[path = "payload.rs"]
pub(crate) mod payload;
#[path = "request.rs"]
pub(crate) mod request;
#[path = "response.rs"]
pub(crate) mod response;
pub use request::{
build_image_request_body, build_image_request_body_with_model,
build_nanobanana_generate_content_request_body, images_edit_url, images_generation_url,
nanobanana_generate_content_url, normalize_image_size_for_model, resolve_image_provider,
};
@@ -1,6 +1,6 @@
use serde_json::Value;
use super::{
use crate::image_provider::{
constants::VECTOR_ENGINE_PROVIDER,
error::PlatformImageError,
util::{ParsedJsonPayload, truncate_raw},
@@ -1,6 +1,6 @@
use serde_json::{Map, Value, json};
use super::{
use crate::image_provider::{
constants::{
GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL,
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_DIMENSION_ALIGNMENT, GPT_IMAGE_2_MAX_EDGE,
@@ -1,4 +1,4 @@
use super::{
use crate::image_provider::{
audit::build_failure_audit,
constants::VECTOR_ENGINE_PROVIDER,
error::PlatformImageError,
@@ -1,4 +1,4 @@
use super::constants::VECTOR_ENGINE_PROVIDER;
use crate::image_provider::constants::VECTOR_ENGINE_PROVIDER;
#[derive(Clone, Debug)]
pub struct PlatformImageFailureAudit {
@@ -1,6 +1,6 @@
use std::time::{Duration, Instant};
use super::{
use crate::image_provider::{
audit::build_failure_audit, constants::VECTOR_ENGINE_PROVIDER, error::PlatformImageError,
};
@@ -1,6 +1,6 @@
use std::{error::Error, fmt};
use super::{audit::PlatformImageFailureAudit, util::is_timeout_message};
use crate::image_provider::{audit::PlatformImageFailureAudit, util::is_timeout_message};
#[derive(Clone, Debug)]
pub enum PlatformImageError {
@@ -4,7 +4,7 @@ const VECTOR_ENGINE_SEND_MAX_ATTEMPTS: u32 = 5;
const VECTOR_ENGINE_SEND_RETRY_BASE_DELAY_MS: u64 = 500;
const VECTOR_ENGINE_SEND_RETRY_MAX_JITTER_MS: u64 = 999;
use super::{
use crate::image_provider::{
budget::{
effective_request_timeout_ms, request_budget_exhausted_error,
retry_delay_fits_request_deadline,
@@ -1060,8 +1060,8 @@ fn image_provider_send_retry_jitter_ms() -> u64 {
#[cfg(test)]
mod tests {
use super::super::types::ImageProvider;
use super::*;
use crate::image_provider::types::ImageProvider;
fn reference_image(index: usize) -> ReferenceImage {
ReferenceImage {
@@ -1107,7 +1107,7 @@ mod tests {
let error = create_nanobanana_generate_content(
&reqwest::Client::new(),
&test_settings(),
super::super::constants::NANOBANANA_2_MODEL,
crate::image_provider::constants::NANOBANANA_2_MODEL,
"测试提示词",
None,
"1:1",
@@ -1163,8 +1163,9 @@ mod tests {
#[test]
fn image_provider_send_retry_policy_treats_ssl_reset_as_transient_transport() {
let error =
super::super::curl_transport::ImageProviderCurlError::Curl(curl::Error::new(35));
let error = crate::image_provider::transport::curl::ImageProviderCurlError::Curl(
curl::Error::new(35),
);
assert!(error.is_transient_transport());
assert!(should_retry_image_curl_send_error(&error, 1));
@@ -1173,8 +1174,9 @@ mod tests {
#[test]
fn image_provider_send_retry_policy_treats_recv_eof_as_transient_transport() {
let error =
super::super::curl_transport::ImageProviderCurlError::Curl(curl::Error::new(56));
let error = crate::image_provider::transport::curl::ImageProviderCurlError::Curl(
curl::Error::new(56),
);
assert!(error.is_transient_transport());
assert!(should_retry_image_curl_send_error(&error, 1));
@@ -2,7 +2,7 @@ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use reqwest::header;
use std::time::Instant;
use super::{
use crate::image_provider::{
budget::request_budget_exhausted_error,
constants::{VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES, VECTOR_ENGINE_PROVIDER},
error::PlatformImageError,
@@ -0,0 +1,25 @@
pub(crate) mod audit;
pub(crate) mod budget;
pub(crate) mod error;
pub(crate) mod executor;
pub(crate) mod image_source;
pub(crate) mod types;
pub(crate) mod util;
pub(crate) use crate::image_provider::transport::curl as curl_transport;
pub use audit::PlatformImageFailureAudit;
pub(crate) use audit::build_failure_audit;
pub(crate) use budget::{effective_request_timeout_ms, request_budget_exhausted_error};
pub use error::{PlatformImageError, PlatformImageStatusHint};
pub use executor::{
create_image_edit, create_image_edit_with_references,
create_image_edit_with_references_and_model, create_image_generation,
create_image_generation_with_model, create_nanobanana_generate_content,
};
pub use image_source::download_remote_image;
pub use types::{
DownloadedImage, GeneratedImages, ImageProvider, ImageProviderClient, ImageProviderSettings,
ReferenceImage,
};
pub(crate) use util::truncate_raw;
@@ -1,4 +1,4 @@
use super::audit::PlatformImageFailureAudit;
use crate::image_provider::audit::PlatformImageFailureAudit;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ImageProvider {
@@ -9,8 +9,8 @@ pub enum ImageProvider {
impl ImageProvider {
pub const fn as_str(self) -> &'static str {
match self {
Self::VectorEngine => super::constants::VECTOR_ENGINE_PROVIDER,
Self::Tiantoken => super::constants::TIANTOKEN_PROVIDER,
Self::VectorEngine => crate::image_provider::constants::VECTOR_ENGINE_PROVIDER,
Self::Tiantoken => crate::image_provider::constants::TIANTOKEN_PROVIDER,
}
}
}
@@ -1,6 +1,6 @@
use serde_json::Value;
use super::{audit::PlatformImageFailureAudit, error::PlatformImageError};
use crate::image_provider::{audit::PlatformImageFailureAudit, error::PlatformImageError};
pub(crate) fn is_timeout_message(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
@@ -1,6 +1,6 @@
use std::time::Duration;
use super::{
use crate::image_provider::{
error::PlatformImageError,
types::{ImageProviderClient, ImageProviderSettings},
};
@@ -6,7 +6,7 @@ use curl::{
};
use serde_json::Value;
use super::{
use crate::image_provider::{
audit::build_failure_audit, constants::VECTOR_ENGINE_PROVIDER, error::PlatformImageError,
request::build_prompt_with_negative, types::ReferenceImage,
};
@@ -0,0 +1,6 @@
#[path = "client.rs"]
pub(crate) mod client;
#[path = "curl.rs"]
pub(crate) mod curl;
pub use client::{build_image_http_client, build_image_provider_client};
+14 -10
View File
@@ -2,25 +2,29 @@ pub mod generated_asset_sheets;
pub mod generated_assets;
pub mod image_provider;
pub mod pixel_art_snapper;
pub mod raw_image_edit;
pub use image_provider::{
DownloadedImage, GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_5_BUSINESS_NAME,
GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_C_MODEL,
GPT_IMAGE_2_MODEL, GeneratedImages, ImageProvider, ImageProviderClient, ImageProviderSettings,
NANOBANANA_2_MODEL, PlatformImageError, PlatformImageFailureAudit, PlatformImageStatusHint,
RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS,
RawImageEditDimensionError, RawImageEditImage, RawImageEditOptions, RawImageEditResult,
ReferenceImage, TIANTOKEN_PROVIDER, VECTOR_ENGINE_PROVIDER, build_image_http_client,
DownloadedImage, GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL,
GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, GeneratedImages,
ImageProvider, ImageProviderClient, ImageProviderSettings, NANOBANANA_2_MODEL,
PlatformImageError, PlatformImageFailureAudit, PlatformImageStatusHint, ReferenceImage,
TIANTOKEN_PROVIDER, VECTOR_ENGINE_PROVIDER, build_image_http_client,
build_image_provider_client, build_image_request_body,
build_nanobanana_generate_content_request_body, create_image_edit,
create_image_edit_with_references, create_image_edit_with_references_and_model,
create_image_generation, create_image_generation_with_model,
create_nanobanana_generate_content, create_raw_image_edit, download_remote_image,
images_edit_url, images_generation_url, nanobanana_generate_content_url,
resolve_image_provider, validate_raw_image_edit_dimensions,
create_nanobanana_generate_content, download_remote_image, images_edit_url,
images_generation_url, nanobanana_generate_content_url, resolve_image_provider,
};
pub use pixel_art_snapper::{
PIXEL_ART_ALPHA_COVERAGE_THRESHOLD, PIXEL_ART_ANALYSIS_COLORS, PIXEL_ART_KMEANS_SAMPLE_LIMIT,
PIXEL_ART_MAX_IMAGE_PIXELS, PixelArtSnapError, snap_pixel_art_strict_with_deadline,
snap_pixel_art_with_deadline,
};
pub use raw_image_edit::{
GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE,
RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditImage,
RawImageEditOptions, RawImageEditResult, create_raw_image_edit,
validate_raw_image_edit_dimensions,
};
@@ -4,17 +4,16 @@ use bytes::Bytes;
use reqwest::multipart::{Form, Part};
use serde::Deserialize;
use super::{
audit::build_failure_audit,
budget::{effective_request_timeout_ms, request_budget_exhausted_error},
use crate::image_provider::{
constants::{
GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_DIMENSION_ALIGNMENT, GPT_IMAGE_2_MAX_EDGE,
GPT_IMAGE_2_MAX_PIXELS, GPT_IMAGE_2_MIN_PIXELS, VECTOR_ENGINE_PROVIDER,
},
error::PlatformImageError,
request::images_edit_url,
types::ImageProviderSettings,
util::truncate_raw,
images_edit_url,
runtime::{
ImageProviderSettings, PlatformImageError, build_failure_audit,
effective_request_timeout_ms, request_budget_exhausted_error, truncate_raw,
},
};
#[derive(Clone, Debug)]