Enforce Genarrative play-type SOP and update docs

Rewrite Genarrative play-type integration guidance across .codex and .hermes to define a platform-level SOP: default to form/image workbench, unify single-image asset slots (CreativeImageInputPanel), standardize series-material sheet->cut->transparent->OSS pipeline, and forbid copying legacy chat/agent workflows as the default. Add decision-log entry freezing the SOP and a pitfalls note warning against direct reuse of old play tools. Update CONTEXT.md and docs/README.md, add a new PRD file, and apply related small server-side changes (module-auth, spacetime-client mappers and runtime) to align back-end code with the new contracts and flows.
This commit is contained in:
2026-05-20 12:12:00 +08:00
parent f370539a6f
commit 3931442249
123 changed files with 15514 additions and 3419 deletions

View File

@@ -0,0 +1,344 @@
use super::*;
pub use shared_contracts::jump_hop::{
JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset,
JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse,
JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus,
JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath,
JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus,
JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse,
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse,
JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse,
JumpHopWorkspaceCreateRequest,
};
pub(crate) fn map_jump_hop_agent_session_procedure_result(
result: JumpHopAgentSessionProcedureResult,
) -> Result<JumpHopSessionSnapshotResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop agent session 快照"))?;
Ok(map_jump_hop_session_snapshot(session))
}
pub(crate) fn map_jump_hop_work_procedure_result(
result: JumpHopWorkProcedureResult,
) -> Result<JumpHopWorkProfileResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let work = result
.work
.ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop work 快照"))?;
map_jump_hop_work_snapshot(work)
}
pub(crate) fn map_jump_hop_works_procedure_result(
result: JumpHopWorksProcedureResult,
) -> Result<Vec<JumpHopWorkProfileResponse>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.items
.into_iter()
.map(map_jump_hop_work_snapshot)
.collect()
}
pub(crate) fn map_jump_hop_run_procedure_result(
result: JumpHopRunProcedureResult,
) -> Result<JumpHopRuntimeRunSnapshotResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let run = result
.run
.ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop run 快照"))?;
Ok(map_jump_hop_run_snapshot(run))
}
pub(crate) fn map_jump_hop_gallery_card_view_row(
row: JumpHopGalleryCardViewRow,
) -> JumpHopGalleryCardResponse {
JumpHopGalleryCardResponse {
public_work_code: row.public_work_code,
work_id: row.work_id,
profile_id: row.profile_id,
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
work_title: row.work_title,
work_description: row.work_description,
cover_image_src: empty_string_to_none(row.cover_image_src),
theme_tags: row.theme_tags,
difficulty: parse_difficulty(&row.difficulty),
style_preset: parse_style_preset(&row.style_preset),
publication_status: normalize_publication_status(&row.publication_status).to_string(),
play_count: row.play_count,
updated_at: format_timestamp_micros(row.updated_at_micros),
published_at: row.published_at_micros.map(format_timestamp_micros),
generation_status: parse_generation_status(&row.generation_status),
}
}
fn map_jump_hop_session_snapshot(
snapshot: JumpHopAgentSessionSnapshot,
) -> JumpHopSessionSnapshotResponse {
JumpHopSessionSnapshotResponse {
session_id: snapshot.session_id,
owner_user_id: snapshot.owner_user_id,
status: snapshot
.draft
.as_ref()
.map(|draft| parse_generation_status(&draft.generation_status))
.unwrap_or(JumpHopGenerationStatus::Draft),
draft: snapshot.draft.map(map_jump_hop_draft_snapshot),
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_jump_hop_work_snapshot(
snapshot: JumpHopWorkSnapshot,
) -> Result<JumpHopWorkProfileResponse, SpacetimeClientError> {
let draft = JumpHopDraftResponse {
template_id: "jump-hop".to_string(),
template_name: "跳一跳".to_string(),
profile_id: Some(snapshot.profile_id.clone()),
work_title: snapshot.work_title.clone(),
work_description: snapshot.work_description.clone(),
theme_tags: snapshot.theme_tags.clone(),
difficulty: parse_difficulty(&snapshot.difficulty),
style_preset: parse_style_preset(&snapshot.style_preset),
character_prompt: snapshot.character_prompt.clone(),
tile_prompt: snapshot.tile_prompt.clone(),
end_mood_prompt: snapshot.end_mood_prompt.clone(),
character_asset: snapshot.character_asset.clone().map(map_character_asset),
tile_atlas_asset: snapshot.tile_atlas_asset.clone().map(map_character_asset),
tile_assets: snapshot
.tile_assets
.clone()
.into_iter()
.map(map_tile_asset)
.collect(),
path: Some(map_jump_hop_path(snapshot.path.clone())),
cover_composite: snapshot.cover_composite.clone(),
generation_status: parse_generation_status(&snapshot.generation_status),
};
let character_asset = draft
.character_asset
.clone()
.ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop character asset"))?;
let tile_atlas_asset = draft
.tile_atlas_asset
.clone()
.ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop tile atlas asset"))?;
Ok(JumpHopWorkProfileResponse {
summary: JumpHopWorkSummaryResponse {
runtime_kind: "jump-hop".to_string(),
work_id: snapshot.work_id,
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
source_session_id: empty_string_to_none(snapshot.source_session_id),
work_title: snapshot.work_title,
work_description: snapshot.work_description,
theme_tags: snapshot.theme_tags,
difficulty: parse_difficulty(&snapshot.difficulty),
style_preset: parse_style_preset(&snapshot.style_preset),
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
publication_status: normalize_publication_status(&snapshot.publication_status)
.to_string(),
play_count: snapshot.play_count,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
publish_ready: snapshot.publish_ready,
generation_status: parse_generation_status(&snapshot.generation_status),
},
draft,
path: map_jump_hop_path(snapshot.path),
character_asset,
tile_atlas_asset,
tile_assets: snapshot.tile_assets.into_iter().map(map_tile_asset).collect(),
})
}
fn map_jump_hop_draft_snapshot(snapshot: JumpHopDraftSnapshot) -> JumpHopDraftResponse {
JumpHopDraftResponse {
template_id: snapshot.template_id,
template_name: snapshot.template_name,
profile_id: snapshot.profile_id,
work_title: snapshot.work_title,
work_description: snapshot.work_description,
theme_tags: snapshot.theme_tags,
difficulty: parse_difficulty(&snapshot.difficulty),
style_preset: parse_style_preset(&snapshot.style_preset),
character_prompt: snapshot.character_prompt,
tile_prompt: snapshot.tile_prompt,
end_mood_prompt: snapshot.end_mood_prompt,
character_asset: snapshot.character_asset.map(map_character_asset),
tile_atlas_asset: snapshot.tile_atlas_asset.map(map_character_asset),
tile_assets: snapshot.tile_assets.into_iter().map(map_tile_asset).collect(),
path: snapshot.path.map(map_jump_hop_path),
cover_composite: snapshot.cover_composite,
generation_status: parse_generation_status(&snapshot.generation_status),
}
}
fn map_character_asset(snapshot: JumpHopCharacterAssetSnapshot) -> JumpHopCharacterAsset {
JumpHopCharacterAsset {
asset_id: snapshot.asset_id,
image_src: snapshot.image_src,
image_object_key: snapshot.image_object_key,
asset_object_id: snapshot.asset_object_id,
generation_provider: snapshot.generation_provider,
prompt: snapshot.prompt,
width: snapshot.width,
height: snapshot.height,
}
}
fn map_tile_asset(snapshot: JumpHopTileAssetSnapshot) -> JumpHopTileAsset {
JumpHopTileAsset {
tile_type: parse_tile_type(&snapshot.tile_type),
image_src: snapshot.image_src,
image_object_key: snapshot.image_object_key,
asset_object_id: snapshot.asset_object_id,
source_atlas_cell: snapshot.source_atlas_cell,
visual_width: snapshot.visual_width,
visual_height: snapshot.visual_height,
top_surface_radius: snapshot.top_surface_radius,
landing_radius: snapshot.landing_radius,
}
}
fn map_jump_hop_path(snapshot: crate::module_bindings::JumpHopPath) -> JumpHopPath {
JumpHopPath {
seed: snapshot.seed,
difficulty: parse_domain_difficulty(snapshot.difficulty),
platforms: snapshot
.platforms
.into_iter()
.map(|platform| JumpHopPlatform {
platform_id: platform.platform_id,
tile_type: parse_domain_tile_type(platform.tile_type),
x: platform.x,
y: platform.y,
width: platform.width,
height: platform.height,
landing_radius: platform.landing_radius,
perfect_radius: platform.perfect_radius,
score_value: platform.score_value,
})
.collect(),
finish_index: snapshot.finish_index,
camera_preset: snapshot.camera_preset,
scoring: JumpHopScoring {
charge_to_distance_ratio: snapshot.scoring.charge_to_distance_ratio,
max_charge_ms: snapshot.scoring.max_charge_ms,
hit_bonus: snapshot.scoring.hit_bonus,
perfect_bonus: snapshot.scoring.perfect_bonus,
},
}
}
fn map_jump_hop_run_snapshot(snapshot: JumpHopRunSnapshot) -> JumpHopRuntimeRunSnapshotResponse {
JumpHopRuntimeRunSnapshotResponse {
run_id: snapshot.run_id,
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
status: match snapshot.status {
crate::module_bindings::JumpHopRunStatus::Failed => JumpHopRunStatus::Failed,
crate::module_bindings::JumpHopRunStatus::Cleared => JumpHopRunStatus::Cleared,
crate::module_bindings::JumpHopRunStatus::Playing => JumpHopRunStatus::Playing,
},
current_platform_index: snapshot.current_platform_index,
score: snapshot.score,
combo: snapshot.combo,
path: map_jump_hop_path(snapshot.path),
last_jump: snapshot.last_jump.map(|jump| JumpHopLastJump {
charge_ms: jump.charge_ms,
jump_distance: jump.jump_distance,
target_platform_index: jump.target_platform_index,
landed_x: jump.landed_x,
landed_y: jump.landed_y,
result: match jump.result {
crate::module_bindings::JumpHopJumpResultKind::Miss => JumpHopJumpResult::Miss,
crate::module_bindings::JumpHopJumpResultKind::Hit => JumpHopJumpResult::Hit,
crate::module_bindings::JumpHopJumpResultKind::Finish => JumpHopJumpResult::Finish,
crate::module_bindings::JumpHopJumpResultKind::Perfect => JumpHopJumpResult::Perfect,
},
}),
started_at_ms: snapshot.started_at_ms,
finished_at_ms: snapshot.finished_at_ms,
}
}
fn parse_difficulty(value: &str) -> JumpHopDifficulty {
match value {
"easy" => JumpHopDifficulty::Easy,
"advanced" => JumpHopDifficulty::Advanced,
"challenge" => JumpHopDifficulty::Challenge,
_ => JumpHopDifficulty::Standard,
}
}
fn parse_domain_difficulty(value: crate::module_bindings::JumpHopDifficulty) -> JumpHopDifficulty {
match value {
crate::module_bindings::JumpHopDifficulty::Easy => JumpHopDifficulty::Easy,
crate::module_bindings::JumpHopDifficulty::Advanced => JumpHopDifficulty::Advanced,
crate::module_bindings::JumpHopDifficulty::Challenge => JumpHopDifficulty::Challenge,
crate::module_bindings::JumpHopDifficulty::Standard => JumpHopDifficulty::Standard,
}
}
fn parse_style_preset(value: &str) -> JumpHopStylePreset {
match value {
"paper-toy" => JumpHopStylePreset::PaperToy,
"neon-glass" => JumpHopStylePreset::NeonGlass,
"forest-stone" => JumpHopStylePreset::ForestStone,
"future-metal" => JumpHopStylePreset::FutureMetal,
"custom" => JumpHopStylePreset::Custom,
_ => JumpHopStylePreset::MinimalBlocks,
}
}
fn parse_tile_type(value: &str) -> JumpHopTileType {
match value {
"start" => JumpHopTileType::Start,
"target" => JumpHopTileType::Target,
"finish" => JumpHopTileType::Finish,
"bonus" => JumpHopTileType::Bonus,
"accent" => JumpHopTileType::Accent,
_ => JumpHopTileType::Normal,
}
}
fn parse_domain_tile_type(value: crate::module_bindings::JumpHopTileType) -> JumpHopTileType {
match value {
crate::module_bindings::JumpHopTileType::Start => JumpHopTileType::Start,
crate::module_bindings::JumpHopTileType::Target => JumpHopTileType::Target,
crate::module_bindings::JumpHopTileType::Finish => JumpHopTileType::Finish,
crate::module_bindings::JumpHopTileType::Bonus => JumpHopTileType::Bonus,
crate::module_bindings::JumpHopTileType::Accent => JumpHopTileType::Accent,
crate::module_bindings::JumpHopTileType::Normal => JumpHopTileType::Normal,
}
}
fn parse_generation_status(value: &str) -> JumpHopGenerationStatus {
match value {
"generating" => JumpHopGenerationStatus::Generating,
"ready" => JumpHopGenerationStatus::Ready,
"failed" => JumpHopGenerationStatus::Failed,
_ => JumpHopGenerationStatus::Draft,
}
}
fn normalize_publication_status(value: &str) -> &str {
match value {
"Published" | "published" => "published",
_ => "draft",
}
}

View File

@@ -213,6 +213,16 @@ pub(crate) fn map_runtime_tracking_event_procedure_result(
Ok(())
}
pub(crate) fn map_runtime_tracking_event_batch_procedure_result(
result: RuntimeTrackingEventBatchProcedureResult,
) -> Result<u32, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result.accepted_count)
}
pub(crate) fn map_runtime_snapshot_procedure_result(
result: RuntimeSnapshotProcedureResult,
) -> Result<Option<RuntimeSnapshotRecord>, SpacetimeClientError> {