feat: integrate jump-hop shelf and asset flow
This commit is contained in:
@@ -226,8 +226,11 @@ impl SpacetimeClient {
|
||||
&self,
|
||||
profile_id: String,
|
||||
) -> Result<JumpHopWorkProfileResponse, SpacetimeClientError> {
|
||||
self.get_jump_hop_work_profile(profile_id, String::new())
|
||||
.await
|
||||
let work = self
|
||||
.get_jump_hop_work_profile(profile_id, String::new())
|
||||
.await?;
|
||||
validate_jump_hop_runtime_ready(&work)?;
|
||||
Ok(work)
|
||||
}
|
||||
|
||||
pub async fn start_jump_hop_run(
|
||||
@@ -235,12 +238,17 @@ impl SpacetimeClient {
|
||||
payload: JumpHopStartRunRequest,
|
||||
owner_user_id: String,
|
||||
) -> Result<JumpHopRuntimeRunSnapshotResponse, SpacetimeClientError> {
|
||||
let profile_id = payload.profile_id;
|
||||
let work = self
|
||||
.get_jump_hop_work_profile(profile_id.clone(), String::new())
|
||||
.await?;
|
||||
validate_jump_hop_runtime_ready(&work)?;
|
||||
let run_id = build_prefixed_uuid_id("jump-hop-run-");
|
||||
let procedure_input = JumpHopRunStartInput {
|
||||
client_event_id: format!("{run_id}:start"),
|
||||
run_id,
|
||||
owner_user_id,
|
||||
profile_id: payload.profile_id,
|
||||
profile_id,
|
||||
started_at_ms: current_unix_micros().div_euclid(1000),
|
||||
};
|
||||
self.start_jump_hop_run_with_input(procedure_input).await
|
||||
@@ -372,11 +380,91 @@ impl SpacetimeClient {
|
||||
&self,
|
||||
public_work_code: String,
|
||||
) -> Result<JumpHopWorkProfileResponse, SpacetimeClientError> {
|
||||
self.get_jump_hop_work_profile(public_work_code, String::new())
|
||||
let gallery = self.list_jump_hop_gallery().await?;
|
||||
let requested_code = normalize_jump_hop_public_work_code(public_work_code.as_str());
|
||||
let card = gallery
|
||||
.items
|
||||
.into_iter()
|
||||
.find(|item| {
|
||||
normalize_jump_hop_public_work_code(item.public_work_code.as_str()) == requested_code
|
||||
})
|
||||
.ok_or_else(|| SpacetimeClientError::Procedure("jump_hop public work 不存在".to_string()))?;
|
||||
|
||||
self.get_jump_hop_work_profile(card.profile_id, String::new())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn validate_jump_hop_runtime_ready(
|
||||
work: &JumpHopWorkProfileResponse,
|
||||
) -> Result<(), SpacetimeClientError> {
|
||||
let status = work.summary.publication_status.trim().to_ascii_lowercase();
|
||||
if status != "published" {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 只能启动已发布作品",
|
||||
));
|
||||
}
|
||||
if work.summary.generation_status != JumpHopGenerationStatus::Ready {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 需要 ready 状态作品",
|
||||
));
|
||||
}
|
||||
validate_jump_hop_character_asset_ready(&work.character_asset, "character_asset")?;
|
||||
validate_jump_hop_character_asset_ready(&work.tile_atlas_asset, "tile_atlas_asset")?;
|
||||
if work.tile_assets.is_empty() {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 缺少地块资产",
|
||||
));
|
||||
}
|
||||
for (index, asset) in work.tile_assets.iter().enumerate() {
|
||||
if asset.image_src.trim().is_empty()
|
||||
|| asset.image_object_key.trim().is_empty()
|
||||
|| asset.asset_object_id.trim().is_empty()
|
||||
{
|
||||
return Err(SpacetimeClientError::validation_failed(format!(
|
||||
"jump-hop runtime 地块资产 #{index} 不完整"
|
||||
)));
|
||||
}
|
||||
}
|
||||
if work.path.platforms.is_empty() {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 缺少可玩路径",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_jump_hop_character_asset_ready(
|
||||
asset: &JumpHopCharacterAsset,
|
||||
field: &str,
|
||||
) -> Result<(), SpacetimeClientError> {
|
||||
if asset.image_src.trim().is_empty()
|
||||
|| asset.image_object_key.trim().is_empty()
|
||||
|| asset.asset_object_id.trim().is_empty()
|
||||
{
|
||||
return Err(SpacetimeClientError::validation_failed(format!(
|
||||
"jump-hop runtime {field} 不完整"
|
||||
)));
|
||||
}
|
||||
if asset.generation_provider.trim().is_empty()
|
||||
|| asset.generation_provider == "deterministic-placeholder"
|
||||
{
|
||||
return Err(SpacetimeClientError::validation_failed(format!(
|
||||
"jump-hop runtime {field} 不是可用真实生成资产"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_jump_hop_public_work_code(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter(|character| character.is_ascii_alphanumeric())
|
||||
.map(|character| character.to_ascii_uppercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
enum JumpHopActionProcedure {
|
||||
Compile(JumpHopDraftCompileInput),
|
||||
Update(JumpHopWorkUpdateInput),
|
||||
@@ -503,22 +591,61 @@ fn merge_action_into_draft(
|
||||
if matches!(
|
||||
scope,
|
||||
JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateCharacter
|
||||
) && let Some(value) = payload
|
||||
.character_prompt
|
||||
.as_ref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
draft.character_prompt = value.trim().to_string();
|
||||
) {
|
||||
if let Some(value) = payload
|
||||
.character_prompt
|
||||
.as_ref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
draft.character_prompt = value.trim().to_string();
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
scope,
|
||||
JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateTiles
|
||||
) && let Some(value) = payload
|
||||
.tile_prompt
|
||||
) {
|
||||
if let Some(value) = payload
|
||||
.tile_prompt
|
||||
.as_ref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
draft.tile_prompt = value.trim().to_string();
|
||||
}
|
||||
}
|
||||
if let Some(profile_id) = payload
|
||||
.profile_id
|
||||
.as_ref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
draft.tile_prompt = value.trim().to_string();
|
||||
draft.profile_id = Some(profile_id.to_string());
|
||||
}
|
||||
if matches!(
|
||||
scope,
|
||||
JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateCharacter
|
||||
) {
|
||||
if let Some(asset) = payload.character_asset.clone() {
|
||||
draft.character_asset = Some(asset);
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
scope,
|
||||
JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateTiles
|
||||
) {
|
||||
if let Some(asset) = payload.tile_atlas_asset.clone() {
|
||||
draft.tile_atlas_asset = Some(asset);
|
||||
}
|
||||
if let Some(assets) = payload.tile_assets.clone() {
|
||||
draft.tile_assets = assets;
|
||||
}
|
||||
}
|
||||
if let Some(value) = payload
|
||||
.cover_composite
|
||||
.as_ref()
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
draft.cover_composite = Some(value.to_string());
|
||||
}
|
||||
if draft.work_title.trim().is_empty() {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
@@ -545,31 +672,30 @@ fn build_compile_input(
|
||||
draft.tile_atlas_asset = None;
|
||||
draft.tile_assets.clear();
|
||||
}
|
||||
let character_asset = ensure_character_asset(
|
||||
draft.character_asset.clone(),
|
||||
let character_asset = draft.character_asset.clone().ok_or_else(|| {
|
||||
SpacetimeClientError::validation_failed(
|
||||
"jump-hop compile-draft 缺少真实角色资产,请先由 api-server 生成并持久化 asset_object",
|
||||
)
|
||||
})?;
|
||||
let tile_atlas_asset = draft.tile_atlas_asset.clone().ok_or_else(|| {
|
||||
SpacetimeClientError::validation_failed(
|
||||
"jump-hop compile-draft 缺少真实地块图集资产,请先由 api-server 生成并持久化 asset_object",
|
||||
)
|
||||
})?;
|
||||
let tile_assets = if draft.tile_assets.is_empty() {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop compile-draft 缺少真实地块资产,请先由 api-server 生成并持久化 asset_object",
|
||||
));
|
||||
} else {
|
||||
draft.tile_assets.clone()
|
||||
};
|
||||
let cover_composite = resolve_cover_composite(
|
||||
draft,
|
||||
profile_id,
|
||||
&draft.character_prompt,
|
||||
force_character,
|
||||
refresh,
|
||||
now_micros,
|
||||
);
|
||||
let tile_atlas_asset = ensure_tile_atlas_asset(
|
||||
draft.tile_atlas_asset.clone(),
|
||||
profile_id,
|
||||
&draft.tile_prompt,
|
||||
force_tiles,
|
||||
now_micros,
|
||||
);
|
||||
let tile_assets = ensure_tile_assets(
|
||||
draft.tile_assets.clone(),
|
||||
profile_id,
|
||||
force_tiles,
|
||||
now_micros,
|
||||
);
|
||||
let cover_composite = resolve_cover_composite(draft, profile_id, refresh, now_micros);
|
||||
|
||||
draft.character_asset = Some(character_asset.clone());
|
||||
draft.tile_atlas_asset = Some(tile_atlas_asset.clone());
|
||||
draft.tile_assets = tile_assets.clone();
|
||||
draft.cover_composite = cover_composite.clone();
|
||||
draft.generation_status = JumpHopGenerationStatus::Ready;
|
||||
|
||||
@@ -698,8 +824,10 @@ fn ensure_character_asset(
|
||||
force_new: bool,
|
||||
now_micros: i64,
|
||||
) -> JumpHopCharacterAsset {
|
||||
if !force_new && let Some(asset) = existing {
|
||||
return asset;
|
||||
if !force_new {
|
||||
if let Some(asset) = existing {
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
let revision = force_new.then_some(now_micros);
|
||||
let suffix = asset_revision_suffix(revision);
|
||||
@@ -722,8 +850,10 @@ fn ensure_tile_atlas_asset(
|
||||
force_new: bool,
|
||||
now_micros: i64,
|
||||
) -> JumpHopCharacterAsset {
|
||||
if !force_new && let Some(asset) = existing {
|
||||
return asset;
|
||||
if !force_new {
|
||||
if let Some(asset) = existing {
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
let revision = force_new.then_some(now_micros);
|
||||
let suffix = asset_revision_suffix(revision);
|
||||
@@ -781,14 +911,15 @@ fn resolve_cover_composite(
|
||||
refresh: JumpHopAssetRefresh,
|
||||
now_micros: i64,
|
||||
) -> Option<String> {
|
||||
if matches!(refresh, JumpHopAssetRefresh::Preserve)
|
||||
&& let Some(value) = draft
|
||||
if matches!(refresh, JumpHopAssetRefresh::Preserve) {
|
||||
if let Some(value) = draft
|
||||
.cover_composite
|
||||
.as_ref()
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return Some(value.to_string());
|
||||
{
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
let suffix = asset_revision_suffix(
|
||||
(!matches!(refresh, JumpHopAssetRefresh::Preserve)).then_some(now_micros),
|
||||
|
||||
Reference in New Issue
Block a user