Merge remote-tracking branch 'origin/master' into codex/bark-battle

This commit is contained in:
kdletters
2026-05-22 05:12:29 +08:00
275 changed files with 29199 additions and 41360 deletions

View File

@@ -229,12 +229,27 @@ pub(super) async fn compile_match3d_draft_for_session(
)
.await?;
let existing_assets = get_match3d_existing_generated_item_assets(
let mut existing_assets = get_match3d_existing_generated_item_assets(
state,
owner_user_id.as_str(),
profile_id.as_str(),
)
.await;
let generated_background_asset = resolve_or_generate_match3d_level_asset_bundle(
state,
request_context,
owner_user_id.as_str(),
session.session_id.as_str(),
profile_id.as_str(),
&config,
generated_work_metadata.background_prompt.as_str(),
&existing_assets,
)
.await?;
attach_match3d_background_asset_to_assets(
&mut existing_assets,
generated_background_asset.clone(),
);
let generated_item_assets = generate_match3d_item_assets(
state,
request_context,
@@ -245,18 +260,22 @@ pub(super) async fn compile_match3d_draft_for_session(
&config,
generated_work_metadata.items,
existing_assets,
Some(generated_background_asset.clone()),
)
.await?;
let generated_item_assets = ensure_match3d_background_asset(
let mut generated_item_assets = generated_item_assets;
attach_match3d_background_asset_to_assets(
&mut generated_item_assets,
generated_background_asset,
);
persist_match3d_generated_item_assets_snapshot(
state,
request_context,
authenticated,
owner_user_id.as_str(),
session.session_id.as_str(),
owner_user_id.as_str(),
profile_id.as_str(),
&config,
generated_work_metadata.background_prompt.as_str(),
generated_item_assets,
&generated_item_assets,
)
.await?;
let existing_cover_image_src = get_match3d_existing_cover_image_src(

File diff suppressed because it is too large Load Diff

View File

@@ -282,13 +282,25 @@ pub(super) fn map_match3d_image_view_from_work(
pub(super) fn map_match3d_background_asset_for_agent(
asset: Match3DGeneratedBackgroundAsset,
) -> shared_contracts::match3d_agent::Match3DGeneratedBackgroundAssetResponse {
let ui_spritesheet_image_src = asset.ui_spritesheet_image_src.clone();
let ui_spritesheet_image_object_key = asset.ui_spritesheet_image_object_key.clone();
shared_contracts::match3d_agent::Match3DGeneratedBackgroundAssetResponse {
prompt: asset.prompt,
level_scene_prompt: asset.level_scene_prompt,
level_scene_image_src: asset.level_scene_image_src,
level_scene_image_object_key: asset.level_scene_image_object_key,
image_src: asset.image_src,
image_object_key: asset.image_object_key,
ui_spritesheet_prompt: asset.ui_spritesheet_prompt,
ui_spritesheet_image_src: ui_spritesheet_image_src.clone(),
ui_spritesheet_image_object_key: ui_spritesheet_image_object_key.clone(),
item_spritesheet_prompt: asset.item_spritesheet_prompt,
item_spritesheet_image_src: asset.item_spritesheet_image_src,
item_spritesheet_image_object_key: asset.item_spritesheet_image_object_key,
container_prompt: asset.container_prompt,
container_image_src: asset.container_image_src,
container_image_object_key: asset.container_image_object_key,
container_image_src: ui_spritesheet_image_src.or(asset.container_image_src),
container_image_object_key: ui_spritesheet_image_object_key
.or(asset.container_image_object_key),
status: asset.status,
error: asset.error,
}
@@ -299,8 +311,17 @@ pub(super) fn map_match3d_background_asset_for_work(
) -> shared_contracts::match3d_works::Match3DGeneratedBackgroundAssetResponse {
shared_contracts::match3d_works::Match3DGeneratedBackgroundAssetResponse {
prompt: asset.prompt,
level_scene_prompt: asset.level_scene_prompt,
level_scene_image_src: asset.level_scene_image_src,
level_scene_image_object_key: asset.level_scene_image_object_key,
image_src: asset.image_src,
image_object_key: asset.image_object_key,
ui_spritesheet_prompt: asset.ui_spritesheet_prompt,
ui_spritesheet_image_src: asset.ui_spritesheet_image_src,
ui_spritesheet_image_object_key: asset.ui_spritesheet_image_object_key,
item_spritesheet_prompt: asset.item_spritesheet_prompt,
item_spritesheet_image_src: asset.item_spritesheet_image_src,
item_spritesheet_image_object_key: asset.item_spritesheet_image_object_key,
container_prompt: asset.container_prompt,
container_image_src: asset.container_image_src,
container_image_object_key: asset.container_image_object_key,
@@ -327,6 +348,14 @@ pub(super) fn resolve_match3d_default_cover_image_src(
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or_else(|| {
asset
.ui_spritesheet_image_src
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
})
.or_else(|| {
asset
.container_image_object_key
@@ -335,6 +364,14 @@ pub(super) fn resolve_match3d_default_cover_image_src(
.filter(|value| !value.is_empty())
.map(str::to_string)
})
.or_else(|| {
asset
.ui_spritesheet_image_object_key
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
})
.or_else(|| {
asset
.image_src
@@ -408,6 +445,10 @@ fn match3d_item_asset_has_image(asset: &Match3DGeneratedItemAssetJson) -> bool {
fn match3d_background_asset_has_image(asset: &Match3DGeneratedBackgroundAsset) -> bool {
match3d_text_present(asset.image_src.as_ref())
|| match3d_text_present(asset.image_object_key.as_ref())
|| match3d_text_present(asset.ui_spritesheet_image_src.as_ref())
|| match3d_text_present(asset.ui_spritesheet_image_object_key.as_ref())
|| match3d_text_present(asset.item_spritesheet_image_src.as_ref())
|| match3d_text_present(asset.item_spritesheet_image_object_key.as_ref())
|| match3d_text_present(asset.container_image_src.as_ref())
|| match3d_text_present(asset.container_image_object_key.as_ref())
}

View File

@@ -26,6 +26,9 @@ pub(super) async fn generate_match3d_material_sheet(
Ok(Match3DMaterialSheet {
task_id: generated.task_id,
prompt,
image_src: None,
image_object_key: None,
image,
})
}

View File

@@ -212,7 +212,7 @@ pub(super) async fn ensure_match3d_background_asset(
}
}
let generated_background = generate_match3d_background_image(
let generated_background = generate_match3d_level_asset_bundle(
state,
owner_user_id,
session_id,
@@ -236,6 +236,40 @@ pub(super) async fn ensure_match3d_background_asset(
Ok(assets)
}
pub(super) async fn resolve_or_generate_match3d_level_asset_bundle(
state: &AppState,
request_context: &RequestContext,
owner_user_id: &str,
session_id: &str,
profile_id: &str,
config: &Match3DConfigJson,
background_prompt: &str,
assets: &[Match3DGeneratedItemAsset],
) -> Result<Match3DGeneratedBackgroundAsset, Response> {
if let Some(existing_background) = find_match3d_generated_background_asset(assets) {
if is_match3d_background_asset_ready(&existing_background) {
return Ok(existing_background);
}
}
let normalized_prompt = normalize_match3d_background_prompt(background_prompt);
let resolved_prompt = if normalized_prompt.is_empty() {
build_fallback_match3d_background_prompt(config)
} else {
normalized_prompt
};
generate_match3d_level_asset_bundle(
state,
owner_user_id,
session_id,
profile_id,
config,
resolved_prompt.as_str(),
)
.await
.map_err(|error| match3d_error_response(request_context, MATCH3D_AGENT_PROVIDER, error))
}
pub(super) fn attach_match3d_background_asset_to_assets(
assets: &mut Vec<Match3DGeneratedItemAsset>,
background_asset: Match3DGeneratedBackgroundAsset,
@@ -281,7 +315,7 @@ pub(super) async fn generate_match3d_cover_image_asset(
create_openai_image_edit(
&http_client,
&settings,
build_match3d_cover_edit_prompt(cover_prompt.as_str()).as_str(),
build_match3d_cover_uploaded_reference_prompt(cover_prompt.as_str()).as_str(),
Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"),
"1:1",
&uploaded_image,
@@ -289,27 +323,38 @@ pub(super) async fn generate_match3d_cover_image_asset(
)
.await?
} else {
let reference_images = resolve_match3d_cover_reference_image_data_urls(
let reference_images = resolve_match3d_cover_reference_images_for_edit(
state,
reference_image_srcs,
MATCH3D_ITEM_IMAGE_MAX_BYTES,
)
.await?;
create_openai_image_generation(
&http_client,
&settings,
build_match3d_cover_reference_generation_prompt(
if reference_images.is_empty() {
create_openai_image_generation(
&http_client,
&settings,
cover_prompt.as_str(),
!reference_images.is_empty(),
Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"),
"1:1",
1,
&[],
"抓大鹅封面图生成失败",
)
.as_str(),
Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"),
"1:1",
1,
reference_images.as_slice(),
"抓大鹅封面图生成失败",
)
.await?
.await?
} else {
create_openai_image_edit_with_references(
&http_client,
&settings,
build_match3d_cover_reference_generation_prompt(cover_prompt.as_str(), true)
.as_str(),
Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"),
"1:1",
1,
reference_images.as_slice(),
"抓大鹅封面图生成失败",
)
.await?
}
};
let image = generated.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
@@ -347,7 +392,7 @@ fn build_match3d_cover_generation_prompt(config: &Match3DConfigJson, prompt: &st
)
}
pub(super) fn build_match3d_cover_edit_prompt(prompt: &str) -> String {
pub(super) fn build_match3d_cover_uploaded_reference_prompt(prompt: &str) -> String {
format!(
concat!(
"请以随请求上传的封面图作为第一优先级重绘依据,保留主图的主体、构图、视角和主要配色;",
@@ -382,24 +427,113 @@ pub(super) async fn generate_match3d_background_image(
profile_id: &str,
config: &Match3DConfigJson,
prompt: &str,
) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
generate_match3d_level_asset_bundle(
state,
owner_user_id,
session_id,
profile_id,
config,
prompt,
)
.await
}
pub(super) async fn generate_match3d_level_asset_bundle(
state: &AppState,
owner_user_id: &str,
session_id: &str,
profile_id: &str,
config: &Match3DConfigJson,
prompt: &str,
) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
require_match3d_oss_client(state)?;
let settings = require_openai_image_settings(state)?;
let http_client = build_openai_image_http_client(&settings)?;
let reference_image = load_match3d_container_reference_image().await?;
let generated_background = create_openai_image_generation(
let level_scene_prompt = build_match3d_level_scene_generation_prompt(config);
let generated_scene = create_openai_image_generation(
&http_client,
&settings,
build_match3d_background_generation_prompt(config, prompt).as_str(),
Some(
"文字、水印、UI、按钮、倒计时、分数、物品、角色、手、边框、教程浮层、菜单、透明区域、透明 alpha、镂空、棋盘格透明底",
),
level_scene_prompt.as_str(),
Some("水印、教程浮层、菜单、广告、真实手机外框、浏览器 UI"),
"9:16",
1,
&[],
"抓大鹅背景图生成失败",
"抓大鹅关卡画面生成失败",
)
.await?;
let level_scene_image = generated_scene.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": "抓大鹅关卡画面生成失败:未返回图片",
}))
})?;
let level_scene_reference = OpenAiReferenceImage {
bytes: level_scene_image.bytes.clone(),
mime_type: level_scene_image.mime_type.clone(),
file_name: "match3d-level-scene.png".to_string(),
};
let level_scene_upload = persist_match3d_generated_bytes(
state,
owner_user_id,
session_id,
profile_id,
&["level-scene", generated_scene.task_id.as_str()],
"scene.png",
level_scene_image.mime_type.as_str(),
level_scene_image.bytes,
"match3d_level_scene_image",
Some(generated_scene.task_id.as_str()),
current_utc_micros(),
)
.await?;
let ui_prompt = build_match3d_ui_spritesheet_prompt();
let background_extract_prompt = build_match3d_background_from_scene_prompt();
let generated_ui_future = create_openai_image_edit(
&http_client,
&settings,
ui_prompt.as_str(),
Some("整页背景、中心物品、容器内物品、重复按钮、文字说明、白底、纯色底、网格线"),
"1:1",
&level_scene_reference,
"抓大鹅 UI spritesheet 生成失败",
);
let generated_background_future = create_openai_image_edit(
&http_client,
&settings,
background_extract_prompt.as_str(),
Some("返回按钮、设置按钮、倒计时、标题文字、道具按钮、物品、容器内含物、菜单、教程浮层"),
"9:16",
&level_scene_reference,
"抓大鹅背景图生成失败",
);
let (generated_ui, generated_background) =
tokio::try_join!(generated_ui_future, generated_background_future)?;
let ui_image = generated_ui.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": "抓大鹅 UI spritesheet 生成失败:未返回图片",
}))
})?;
let ui_image = make_match3d_spritesheet_image_transparent(ui_image)?;
let ui_upload = persist_match3d_generated_bytes(
state,
owner_user_id,
session_id,
profile_id,
&["ui-spritesheet", generated_ui.task_id.as_str()],
"ui-spritesheet.png",
ui_image.mime_type.as_str(),
ui_image.bytes,
"match3d_ui_spritesheet_image",
Some(generated_ui.task_id.as_str()),
current_utc_micros(),
)
.await?;
let background_image = generated_background
.images
.into_iter()
@@ -426,50 +560,22 @@ pub(super) async fn generate_match3d_background_image(
)
.await?;
let container_prompt = build_match3d_container_generation_prompt(config, prompt);
let generated_container = create_openai_image_edit(
&http_client,
&settings,
container_prompt.as_str(),
Some("文字、水印、按钮、倒计时、分数、物品、角色、手、教程浮层、菜单、整页背景、小容器、正俯视圆盘、侧视碗、餐盘、托盘、画布大留白"),
"1:1",
&reference_image,
"抓大鹅容器 UI 图生成失败",
)
.await?;
let container_image = generated_container
.images
.into_iter()
.next()
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": "抓大鹅容器 UI 图生成失败:未返回图片",
}))
})?;
let container_image = make_match3d_container_image_transparent(container_image)?;
let container_upload = persist_match3d_generated_bytes(
state,
owner_user_id,
session_id,
profile_id,
&["ui-container", generated_container.task_id.as_str()],
"container.png",
container_image.mime_type.as_str(),
container_image.bytes,
"match3d_ui_container_image",
Some(generated_container.task_id.as_str()),
current_utc_micros(),
)
.await?;
Ok(Match3DGeneratedBackgroundAsset {
prompt: prompt.to_string(),
level_scene_prompt: Some(level_scene_prompt),
level_scene_image_src: Some(level_scene_upload.src),
level_scene_image_object_key: Some(level_scene_upload.object_key),
image_src: Some(background_upload.src),
image_object_key: Some(background_upload.object_key),
container_prompt: Some(container_prompt),
container_image_src: Some(container_upload.src),
container_image_object_key: Some(container_upload.object_key),
ui_spritesheet_prompt: Some(ui_prompt.clone()),
ui_spritesheet_image_src: Some(ui_upload.src.clone()),
ui_spritesheet_image_object_key: Some(ui_upload.object_key.clone()),
item_spritesheet_prompt: None,
item_spritesheet_image_src: None,
item_spritesheet_image_object_key: None,
container_prompt: Some(ui_prompt),
container_image_src: Some(ui_upload.src),
container_image_object_key: Some(ui_upload.object_key),
status: "image_ready".to_string(),
error: None,
})
@@ -486,7 +592,7 @@ pub(super) async fn generate_match3d_container_image(
require_match3d_oss_client(state)?;
let settings = require_openai_image_settings(state)?;
let http_client = build_openai_image_http_client(&settings)?;
let reference_image = load_match3d_container_reference_image().await?;
let reference_image = load_match3d_container_reference_image()?;
let container_prompt = build_match3d_container_generation_prompt(config, prompt);
let generated_container = create_openai_image_edit(
&http_client,
@@ -533,6 +639,7 @@ pub(super) async fn generate_match3d_container_image(
container_image_object_key: Some(container_upload.object_key),
status: "image_ready".to_string(),
error: None,
..Default::default()
})
}
@@ -549,12 +656,39 @@ pub(super) fn merge_match3d_container_image_into_background_asset(
.unwrap_or_else(|| container_asset.prompt.clone());
Match3DGeneratedBackgroundAsset {
prompt,
level_scene_prompt: existing_background
.as_ref()
.and_then(|asset| asset.level_scene_prompt.clone()),
level_scene_image_src: existing_background
.as_ref()
.and_then(|asset| asset.level_scene_image_src.clone()),
level_scene_image_object_key: existing_background
.as_ref()
.and_then(|asset| asset.level_scene_image_object_key.clone()),
image_src: existing_background
.as_ref()
.and_then(|asset| asset.image_src.clone()),
image_object_key: existing_background
.as_ref()
.and_then(|asset| asset.image_object_key.clone()),
ui_spritesheet_prompt: existing_background
.as_ref()
.and_then(|asset| asset.ui_spritesheet_prompt.clone()),
ui_spritesheet_image_src: existing_background
.as_ref()
.and_then(|asset| asset.ui_spritesheet_image_src.clone()),
ui_spritesheet_image_object_key: existing_background
.as_ref()
.and_then(|asset| asset.ui_spritesheet_image_object_key.clone()),
item_spritesheet_prompt: existing_background
.as_ref()
.and_then(|asset| asset.item_spritesheet_prompt.clone()),
item_spritesheet_image_src: existing_background
.as_ref()
.and_then(|asset| asset.item_spritesheet_image_src.clone()),
item_spritesheet_image_object_key: existing_background
.as_ref()
.and_then(|asset| asset.item_spritesheet_image_object_key.clone()),
container_prompt: container_asset.container_prompt,
container_image_src: container_asset.container_image_src,
container_image_object_key: container_asset.container_image_object_key,
@@ -563,15 +697,10 @@ pub(super) fn merge_match3d_container_image_into_background_asset(
}
}
async fn load_match3d_container_reference_image() -> Result<OpenAiReferenceImage, AppError> {
let bytes = tokio::fs::read(MATCH3D_CONTAINER_REFERENCE_IMAGE_PATH)
.await
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": MATCH3D_AGENT_PROVIDER,
"message": format!("读取抓大鹅容器参考图失败:{error}"),
}))
})?;
pub(super) fn load_match3d_container_reference_image() -> Result<OpenAiReferenceImage, AppError> {
// 中文注释:生产 API 单独发布二进制Web 静态资源可能在另一轮流水线发布。
// 容器参考图属于后端生图协议输入,必须随 api-server 编译进二进制,不能依赖运行时 cwd 下存在 public/。
let bytes = MATCH3D_CONTAINER_REFERENCE_IMAGE_BYTES.to_vec();
if bytes.is_empty() {
return Err(
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
@@ -766,6 +895,32 @@ pub(super) fn make_match3d_container_image_transparent(
extension: "png".to_string(),
})
}
pub(super) fn make_match3d_spritesheet_image_transparent(
image: DownloadedOpenAiImage,
) -> Result<DownloadedOpenAiImage, AppError> {
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "match3d-assets",
"message": format!("抓大鹅 spritesheet 图解码失败:{error}"),
}))
})?;
let mut encoded = std::io::Cursor::new(Vec::new());
apply_generated_asset_sheet_green_screen_alpha(source)
.write_to(&mut encoded, ImageFormat::Png)
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "match3d-assets",
"message": format!("抓大鹅 spritesheet 图透明化失败:{error}"),
}))
})?;
Ok(DownloadedOpenAiImage {
bytes: encoded.into_inner(),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
})
}
pub(super) async fn download_match3d_legacy_model(
file: &hyper3d_contract::Hyper3dDownloadFilePayload,
) -> Result<Match3DDownloadedModel, AppError> {
@@ -869,7 +1024,7 @@ pub(super) fn is_match3d_glb_binary_payload(bytes: &[u8]) -> bool {
magic == 0x4654_6c67 && version == 2 && declared_length == bytes.len()
}
async fn read_match3d_generated_object_bytes(
pub(super) async fn read_match3d_generated_object_bytes(
state: &AppState,
object_key: &str,
message_prefix: &str,
@@ -920,57 +1075,6 @@ async fn read_match3d_generated_object_bytes(
Ok(bytes.to_vec())
}
async fn resolve_match3d_reference_image_data_url(
state: &AppState,
source: Option<&str>,
max_size_bytes: usize,
) -> Result<Option<String>, AppError> {
let Some(source) = source.map(str::trim).filter(|value| !value.is_empty()) else {
return Ok(None);
};
if source.starts_with("data:image/") {
return Ok(Some(source.to_string()));
}
if let Some(public_path) = normalize_match3d_public_reference_image_path(source) {
let bytes = tokio::fs::read(public_path.as_str())
.await
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": MATCH3D_WORKS_PROVIDER,
"message": format!("读取抓大鹅本地参考图失败:{error}"),
"path": public_path,
}))
})?;
if bytes.is_empty() || bytes.len() > max_size_bytes {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": MATCH3D_WORKS_PROVIDER,
"field": "referenceImageSrcs",
"message": "封面参考图过大,请压缩后重试。",
"maxBytes": max_size_bytes,
"actualBytes": bytes.len(),
})),
);
}
return Ok(Some(format!(
"data:{};base64,{}",
infer_match3d_image_mime_type(bytes.as_slice()),
BASE64_STANDARD.encode(bytes)
)));
}
if !source.trim_start_matches('/').starts_with("generated-") {
return Ok(Some(source.to_string()));
}
let bytes =
read_match3d_generated_object_bytes(state, source, "读取抓大鹅参考图失败", max_size_bytes)
.await?;
Ok(Some(format!(
"data:{};base64,{}",
infer_match3d_image_mime_type(bytes.as_slice()),
BASE64_STANDARD.encode(bytes)
)))
}
pub(super) fn normalize_match3d_public_reference_image_path(source: &str) -> Option<String> {
let source = source
.trim()
@@ -992,7 +1096,9 @@ pub(super) fn normalize_match3d_public_reference_image_path(source: &str) -> Opt
) {
return None;
}
Some(format!("public/{source}"))
Some(format!(
"{MATCH3D_PUBLIC_REFERENCE_IMAGE_PATH_PREFIX}{source}"
))
}
pub(super) fn collect_match3d_cover_reference_image_sources(
@@ -1021,18 +1127,22 @@ pub(super) fn collect_match3d_cover_reference_image_sources(
sources
}
async fn resolve_match3d_cover_reference_image_data_urls(
async fn resolve_match3d_cover_reference_images_for_edit(
state: &AppState,
sources: Vec<String>,
max_size_bytes: usize,
) -> Result<Vec<String>, AppError> {
) -> Result<Vec<OpenAiReferenceImage>, AppError> {
let mut resolved = Vec::new();
for source in sources {
if let Some(data_url) =
resolve_match3d_reference_image_data_url(state, Some(source.as_str()), max_size_bytes)
.await?
for (index, source) in sources.into_iter().enumerate() {
if let Some(image) = resolve_match3d_reference_image_for_edit(
state,
Some(source.as_str()),
max_size_bytes,
format!("match3d-cover-reference-{index}").as_str(),
)
.await?
{
resolved.push(data_url);
resolved.push(image);
}
}
Ok(resolved)
@@ -1049,6 +1159,16 @@ async fn resolve_match3d_reference_image_for_edit(
};
let bytes = if source.starts_with("data:image/") {
decode_match3d_data_url_bytes(source)?
} else if let Some(public_path) = normalize_match3d_public_reference_image_path(source) {
tokio::fs::read(public_path.as_str())
.await
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": MATCH3D_WORKS_PROVIDER,
"message": format!("读取抓大鹅本地参考图失败:{error}"),
"path": public_path,
}))
})?
} else if source.trim_start_matches('/').starts_with("generated-") {
read_match3d_generated_object_bytes(
state,
@@ -1062,7 +1182,7 @@ async fn resolve_match3d_reference_image_for_edit(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": MATCH3D_WORKS_PROVIDER,
"field": "uploadedImageSrc",
"message": "封面上传图必须是图片 Data URL 或 /generated-* 路径。",
"message": "封面参考图必须是图片 Data URL、本地 public 参考图或 /generated-* 路径。",
})),
);
};
@@ -1089,7 +1209,7 @@ async fn resolve_match3d_reference_image_for_edit(
}))
}
fn decode_match3d_data_url_bytes(source: &str) -> Result<Vec<u8>, AppError> {
pub(super) fn decode_match3d_data_url_bytes(source: &str) -> Result<Vec<u8>, AppError> {
let Some((header, data)) = source.split_once(',') else {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({