Switch to VectorEngine gpt-image-2 and edits

Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
This commit is contained in:
2026-05-22 03:06:41 +08:00
parent 321e1ea33a
commit ae014ac881
90 changed files with 7078 additions and 3389 deletions

View File

@@ -5,11 +5,11 @@ use crate::mapper::{
map_jump_hop_works_procedure_result,
};
use shared_contracts::jump_hop::{
JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset, JumpHopDifficulty,
JumpHopDraftResponse, JumpHopGalleryResponse, JumpHopGenerationStatus, JumpHopJumpRequest,
JumpHopRestartRunRequest, JumpHopRuntimeRunSnapshotResponse, JumpHopSessionSnapshotResponse,
JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset, JumpHopTileType,
JumpHopWorkProfileResponse,
JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset,
JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryResponse, JumpHopGenerationStatus,
JumpHopJumpRequest, JumpHopRestartRunRequest, JumpHopRuntimeRunSnapshotResponse,
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
JumpHopTileType, JumpHopWorkProfileResponse,
};
use shared_kernel::build_prefixed_uuid_id;
@@ -21,10 +21,9 @@ impl SpacetimeClient {
&self,
session: JumpHopSessionSnapshotResponse,
) -> Result<JumpHopSessionSnapshotResponse, SpacetimeClientError> {
let draft = session
.draft
.clone()
.ok_or_else(|| SpacetimeClientError::validation_failed("jump-hop session 缺少 draft"))?;
let draft = session.draft.clone().ok_or_else(|| {
SpacetimeClientError::validation_failed("jump-hop session 缺少 draft")
})?;
let theme_tags_json = Some(json_string(&draft.theme_tags)?);
let config_json = Some(build_config_json(&draft)?);
let work_title = draft.work_title.clone();
@@ -164,15 +163,14 @@ impl SpacetimeClient {
procedure_input: JumpHopWorkUpdateInput,
) -> Result<JumpHopWorkProfileResponse, SpacetimeClientError> {
self.call_after_connect("update_jump_hop_work", move |connection, sender| {
connection.procedures().update_jump_hop_work_then(
procedure_input,
move |_, result| {
connection
.procedures()
.update_jump_hop_work_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_jump_hop_work_procedure_result);
send_once(&sender, mapped);
},
);
});
})
.await
}
@@ -212,15 +210,14 @@ impl SpacetimeClient {
};
self.call_after_connect("list_jump_hop_works", move |connection, sender| {
connection.procedures().list_jump_hop_works_then(
procedure_input,
move |_, result| {
connection
.procedures()
.list_jump_hop_works_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_jump_hop_works_procedure_result);
send_once(&sender, mapped);
},
);
});
})
.await
}
@@ -229,7 +226,8 @@ impl SpacetimeClient {
&self,
profile_id: String,
) -> Result<JumpHopWorkProfileResponse, SpacetimeClientError> {
self.get_jump_hop_work_profile(profile_id, String::new()).await
self.get_jump_hop_work_profile(profile_id, String::new())
.await
}
pub async fn start_jump_hop_run(
@@ -253,15 +251,14 @@ impl SpacetimeClient {
procedure_input: JumpHopRunStartInput,
) -> Result<JumpHopRuntimeRunSnapshotResponse, SpacetimeClientError> {
self.call_after_connect("start_jump_hop_run", move |connection, sender| {
connection.procedures().start_jump_hop_run_then(
procedure_input,
move |_, result| {
connection
.procedures()
.start_jump_hop_run_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_jump_hop_run_procedure_result);
send_once(&sender, mapped);
},
);
});
})
.await
}
@@ -277,15 +274,14 @@ impl SpacetimeClient {
};
self.call_after_connect("get_jump_hop_run", move |connection, sender| {
connection.procedures().get_jump_hop_run_then(
procedure_input,
move |_, result| {
connection
.procedures()
.get_jump_hop_run_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_jump_hop_run_procedure_result);
send_once(&sender, mapped);
},
);
});
})
.await
}
@@ -305,15 +301,14 @@ impl SpacetimeClient {
};
self.call_after_connect("jump_hop_jump", move |connection, sender| {
connection.procedures().jump_hop_jump_then(
procedure_input,
move |_, result| {
connection
.procedures()
.jump_hop_jump_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_jump_hop_run_procedure_result);
send_once(&sender, mapped);
},
);
});
})
.await
}
@@ -333,15 +328,14 @@ impl SpacetimeClient {
};
self.call_after_connect("restart_jump_hop_run", move |connection, sender| {
connection.procedures().restart_jump_hop_run_then(
procedure_input,
move |_, result| {
connection
.procedures()
.restart_jump_hop_run_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_jump_hop_run_procedure_result);
send_once(&sender, mapped);
},
);
});
})
.await
}
@@ -430,16 +424,16 @@ fn build_jump_hop_action_plan(
JumpHopAssetRefresh::Preserve,
now_micros,
)?),
JumpHopActionType::RegenerateCharacter => JumpHopActionProcedure::Compile(
build_compile_input(
JumpHopActionType::RegenerateCharacter => {
JumpHopActionProcedure::Compile(build_compile_input(
current,
owner_user_id,
&profile_id,
&mut draft,
JumpHopAssetRefresh::Character,
now_micros,
)?,
),
)?)
}
JumpHopActionType::RegenerateTiles => JumpHopActionProcedure::Compile(build_compile_input(
current,
owner_user_id,
@@ -472,7 +466,11 @@ fn merge_action_into_draft(
scope,
JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::UpdateWorkMeta
) {
if let Some(value) = payload.work_title.as_ref().filter(|value| !value.trim().is_empty()) {
if let Some(value) = payload
.work_title
.as_ref()
.filter(|value| !value.trim().is_empty())
{
draft.work_title = value.trim().to_string();
}
if let Some(value) = payload.work_description.as_ref() {
@@ -523,7 +521,9 @@ fn merge_action_into_draft(
draft.tile_prompt = value.trim().to_string();
}
if draft.work_title.trim().is_empty() {
return Err(SpacetimeClientError::validation_failed("jump-hop work_title 不能为空"));
return Err(SpacetimeClientError::validation_failed(
"jump-hop work_title 不能为空",
));
}
Ok(draft)
}
@@ -762,7 +762,9 @@ fn ensure_tile_assets(
.map(|(index, tile_type)| JumpHopTileAsset {
tile_type,
image_src: format!("/generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png"),
image_object_key: format!("generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png"),
image_object_key: format!(
"generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png"
),
asset_object_id: format!("{profile_id}-tile-{index}{suffix}-object"),
source_atlas_cell: format!("cell-{index}{suffix}"),
visual_width: 256,
@@ -788,7 +790,9 @@ fn resolve_cover_composite(
{
return Some(value.to_string());
}
let suffix = asset_revision_suffix((!matches!(refresh, JumpHopAssetRefresh::Preserve)).then_some(now_micros));
let suffix = asset_revision_suffix(
(!matches!(refresh, JumpHopAssetRefresh::Preserve)).then_some(now_micros),
);
Some(format!(
"/generated-jump-hop-assets/{profile_id}/cover-composite{suffix}.png"
))
@@ -850,9 +854,27 @@ mod tests {
assert_eq!(input.session_id, SESSION_ID);
assert_eq!(input.owner_user_id, OWNER_USER_ID);
assert_eq!(input.generation_status.as_deref(), Some("ready"));
assert!(input.character_asset_json.as_deref().unwrap_or("").contains("-character"));
assert!(input.tile_atlas_asset_json.as_deref().unwrap_or("").contains("-tile-atlas"));
assert!(input.tile_assets_json.as_deref().unwrap_or("").contains("tile-0-object"));
assert!(
input
.character_asset_json
.as_deref()
.unwrap_or("")
.contains("-character")
);
assert!(
input
.tile_atlas_asset_json
.as_deref()
.unwrap_or("")
.contains("-tile-atlas")
);
assert!(
input
.tile_assets_json
.as_deref()
.unwrap_or("")
.contains("tile-0-object")
);
assert_eq!(draft.generation_status, JumpHopGenerationStatus::Ready);
}
@@ -869,10 +891,34 @@ mod tests {
let JumpHopActionProcedure::Compile(input) = plan else {
panic!("regenerate-character should call compile_jump_hop_draft");
};
assert!(!input.character_asset_json.as_deref().unwrap_or("").contains("old-character"));
assert!(input.character_asset_json.as_deref().unwrap_or("").contains(&NOW_MICROS.to_string()));
assert!(input.tile_atlas_asset_json.as_deref().unwrap_or("").contains("old-tile-atlas"));
assert!(input.tile_assets_json.as_deref().unwrap_or("").contains("old-normal-tile"));
assert!(
!input
.character_asset_json
.as_deref()
.unwrap_or("")
.contains("old-character")
);
assert!(
input
.character_asset_json
.as_deref()
.unwrap_or("")
.contains(&NOW_MICROS.to_string())
);
assert!(
input
.tile_atlas_asset_json
.as_deref()
.unwrap_or("")
.contains("old-tile-atlas")
);
assert!(
input
.tile_assets_json
.as_deref()
.unwrap_or("")
.contains("old-normal-tile")
);
}
#[test]
@@ -888,11 +934,41 @@ mod tests {
let JumpHopActionProcedure::Compile(input) = plan else {
panic!("regenerate-tiles should call compile_jump_hop_draft");
};
assert!(input.character_asset_json.as_deref().unwrap_or("").contains("old-character"));
assert!(!input.tile_atlas_asset_json.as_deref().unwrap_or("").contains("old-tile-atlas"));
assert!(!input.tile_assets_json.as_deref().unwrap_or("").contains("old-normal-tile"));
assert!(input.tile_atlas_asset_json.as_deref().unwrap_or("").contains(&NOW_MICROS.to_string()));
assert!(input.tile_assets_json.as_deref().unwrap_or("").contains(&NOW_MICROS.to_string()));
assert!(
input
.character_asset_json
.as_deref()
.unwrap_or("")
.contains("old-character")
);
assert!(
!input
.tile_atlas_asset_json
.as_deref()
.unwrap_or("")
.contains("old-tile-atlas")
);
assert!(
!input
.tile_assets_json
.as_deref()
.unwrap_or("")
.contains("old-normal-tile")
);
assert!(
input
.tile_atlas_asset_json
.as_deref()
.unwrap_or("")
.contains(&NOW_MICROS.to_string())
);
assert!(
input
.tile_assets_json
.as_deref()
.unwrap_or("")
.contains(&NOW_MICROS.to_string())
);
}
#[test]
@@ -934,8 +1010,20 @@ mod tests {
};
assert_eq!(input.difficulty.as_deref(), Some("challenge"));
assert!(input.style_preset.is_none());
assert_eq!(draft.character_asset.as_ref().map(|asset| asset.asset_id.as_str()), Some("old-character"));
assert_eq!(draft.tile_assets.first().map(|asset| asset.asset_object_id.as_str()), Some("old-normal-tile-object"));
assert_eq!(
draft
.character_asset
.as_ref()
.map(|asset| asset.asset_id.as_str()),
Some("old-character")
);
assert_eq!(
draft
.tile_assets
.first()
.map(|asset| asset.asset_object_id.as_str()),
Some("old-normal-tile-object")
);
}
fn action(action_type: JumpHopActionType) -> JumpHopActionRequest {