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.
582 lines
20 KiB
Rust
582 lines
20 KiB
Rust
use super::*;
|
||
|
||
pub(super) fn map_match3d_agent_session_response(
|
||
session: Match3DAgentSessionRecord,
|
||
) -> Match3DAgentSessionSnapshotResponse {
|
||
Match3DAgentSessionSnapshotResponse {
|
||
session_id: session.session_id,
|
||
current_turn: session.current_turn,
|
||
progress_percent: session.progress_percent,
|
||
stage: session.stage.clone(),
|
||
anchor_pack: map_match3d_anchor_pack_response_for_turn(
|
||
session.anchor_pack,
|
||
session.current_turn,
|
||
session.stage.as_str(),
|
||
),
|
||
config: session.config.map(map_match3d_config_response),
|
||
draft: session.draft.map(map_match3d_draft_response),
|
||
messages: session
|
||
.messages
|
||
.into_iter()
|
||
.map(map_match3d_message_response)
|
||
.collect(),
|
||
last_assistant_reply: session.last_assistant_reply,
|
||
published_profile_id: session.published_profile_id,
|
||
updated_at: session.updated_at,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_agent_session_response_with_assets(
|
||
session: Match3DAgentSessionRecord,
|
||
generated_item_assets: &[Match3DGeneratedItemAsset],
|
||
) -> Match3DAgentSessionSnapshotResponse {
|
||
let mut response = map_match3d_agent_session_response(session);
|
||
if let Some(draft) = response.draft.as_mut() {
|
||
if generated_item_assets.is_empty() {
|
||
return response;
|
||
}
|
||
|
||
draft.generated_item_assets = generated_item_assets
|
||
.iter()
|
||
.cloned()
|
||
.map(map_match3d_generated_item_asset_for_agent)
|
||
.collect();
|
||
if draft
|
||
.cover_image_src
|
||
.as_deref()
|
||
.map(str::trim)
|
||
.unwrap_or_default()
|
||
.is_empty()
|
||
{
|
||
draft.cover_image_src = resolve_match3d_default_cover_image_src(generated_item_assets);
|
||
}
|
||
let background_asset = find_match3d_generated_background_asset(generated_item_assets);
|
||
apply_match3d_background_asset_to_agent_draft(draft, background_asset);
|
||
}
|
||
response
|
||
}
|
||
|
||
pub(super) fn map_match3d_anchor_pack_response_for_turn(
|
||
anchor: Match3DAnchorPackRecord,
|
||
current_turn: u32,
|
||
stage: &str,
|
||
) -> Match3DAnchorPackResponse {
|
||
let is_ready = matches!(
|
||
stage,
|
||
"ReadyToCompile"
|
||
| "ready_to_compile"
|
||
| "DraftCompiled"
|
||
| "draft_compiled"
|
||
| "draft_ready"
|
||
| "ReadyToPublish"
|
||
| "ready_to_publish"
|
||
| "Published"
|
||
| "published"
|
||
);
|
||
let collected_count = if is_ready { 3 } else { current_turn.min(3) };
|
||
|
||
Match3DAnchorPackResponse {
|
||
theme: map_match3d_anchor_item_response_for_collected(anchor.theme, collected_count >= 1),
|
||
clear_count: map_match3d_anchor_item_response_for_collected(
|
||
anchor.clear_count,
|
||
collected_count >= 2,
|
||
),
|
||
difficulty: map_match3d_anchor_item_response_for_collected(
|
||
anchor.difficulty,
|
||
collected_count >= 3,
|
||
),
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_anchor_item_response(
|
||
anchor: Match3DAnchorItemRecord,
|
||
) -> Match3DAnchorItemResponse {
|
||
Match3DAnchorItemResponse {
|
||
key: anchor.key,
|
||
label: anchor.label,
|
||
value: anchor.value,
|
||
status: anchor.status,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_anchor_item_response_for_collected(
|
||
anchor: Match3DAnchorItemRecord,
|
||
collected: bool,
|
||
) -> Match3DAnchorItemResponse {
|
||
if collected {
|
||
return map_match3d_anchor_item_response(anchor);
|
||
}
|
||
|
||
Match3DAnchorItemResponse {
|
||
key: anchor.key,
|
||
label: anchor.label,
|
||
value: String::new(),
|
||
status: "missing".to_string(),
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_config_response(
|
||
config: Match3DCreatorConfigRecord,
|
||
) -> Match3DCreatorConfigResponse {
|
||
Match3DCreatorConfigResponse {
|
||
theme_text: config.theme_text,
|
||
reference_image_src: config.reference_image_src,
|
||
clear_count: config.clear_count,
|
||
difficulty: config.difficulty,
|
||
asset_style_id: config.asset_style_id,
|
||
asset_style_label: config.asset_style_label,
|
||
asset_style_prompt: config.asset_style_prompt,
|
||
generate_click_sound: config.generate_click_sound,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_draft_response(
|
||
draft: Match3DResultDraftRecord,
|
||
) -> Match3DResultDraftResponse {
|
||
// 中文注释:session draft 自身也可能携带生成素材快照,不能只依赖 work detail 回读补齐 UI 背景和容器图。
|
||
let generated_item_assets =
|
||
parse_match3d_generated_item_assets(draft.generated_item_assets_json.as_deref())
|
||
.into_iter()
|
||
.map(Match3DGeneratedItemAsset::from)
|
||
.collect::<Vec<_>>();
|
||
let background_asset = find_match3d_generated_background_asset(&generated_item_assets);
|
||
let mut response = Match3DResultDraftResponse {
|
||
profile_id: draft.profile_id,
|
||
game_name: draft.game_name,
|
||
theme_text: draft.theme_text,
|
||
summary_text: Some(draft.summary_text.clone()),
|
||
summary: draft.summary_text,
|
||
tags: draft.tags,
|
||
cover_image_src: draft.cover_image_src,
|
||
reference_image_src: draft.reference_image_src,
|
||
clear_count: draft.clear_count,
|
||
difficulty: draft.difficulty,
|
||
total_item_count: draft.total_item_count,
|
||
publish_ready: draft.publish_ready,
|
||
blockers: draft.blockers,
|
||
background_prompt: None,
|
||
background_image_src: None,
|
||
background_image_object_key: None,
|
||
generated_background_asset: None,
|
||
generated_item_assets: generated_item_assets
|
||
.iter()
|
||
.cloned()
|
||
.map(map_match3d_generated_item_asset_for_agent)
|
||
.collect(),
|
||
};
|
||
|
||
if response
|
||
.cover_image_src
|
||
.as_deref()
|
||
.map(str::trim)
|
||
.unwrap_or_default()
|
||
.is_empty()
|
||
{
|
||
response.cover_image_src = resolve_match3d_default_cover_image_src(&generated_item_assets);
|
||
}
|
||
apply_match3d_background_asset_to_agent_draft(&mut response, background_asset);
|
||
response
|
||
}
|
||
|
||
pub(super) fn map_match3d_generated_item_asset_for_agent(
|
||
asset: Match3DGeneratedItemAsset,
|
||
) -> Match3DAgentGeneratedItemAssetResponse {
|
||
Match3DAgentGeneratedItemAssetResponse {
|
||
item_id: asset.item_id,
|
||
item_name: asset.item_name,
|
||
item_size: asset.item_size,
|
||
image_src: asset.image_src,
|
||
image_object_key: asset.image_object_key,
|
||
image_views: asset
|
||
.image_views
|
||
.into_iter()
|
||
.map(map_match3d_image_view_for_agent)
|
||
.collect(),
|
||
model_src: asset.model_src,
|
||
model_object_key: asset.model_object_key,
|
||
model_file_name: asset.model_file_name,
|
||
task_uuid: asset.task_uuid,
|
||
subscription_key: asset.subscription_key,
|
||
sound_prompt: asset.sound_prompt,
|
||
background_music_title: asset.background_music_title,
|
||
background_music_style: asset.background_music_style,
|
||
background_music_prompt: asset.background_music_prompt,
|
||
background_music: asset.background_music,
|
||
click_sound: asset.click_sound,
|
||
background_asset: asset
|
||
.background_asset
|
||
.map(map_match3d_background_asset_for_agent),
|
||
status: asset.status,
|
||
error: asset.error,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_generated_item_asset_for_work(
|
||
asset: Match3DGeneratedItemAssetJson,
|
||
) -> shared_contracts::match3d_works::Match3DGeneratedItemAssetResponse {
|
||
shared_contracts::match3d_works::Match3DGeneratedItemAssetResponse {
|
||
item_id: asset.item_id,
|
||
item_name: asset.item_name,
|
||
item_size: asset
|
||
.item_size
|
||
.or_else(|| Some(MATCH3D_ITEM_SIZE_LARGE.to_string())),
|
||
image_src: asset.image_src,
|
||
image_object_key: asset.image_object_key,
|
||
image_views: asset
|
||
.image_views
|
||
.into_iter()
|
||
.map(map_match3d_image_view_for_work)
|
||
.collect(),
|
||
model_src: asset.model_src,
|
||
model_object_key: asset.model_object_key,
|
||
model_file_name: asset.model_file_name,
|
||
task_uuid: asset.task_uuid,
|
||
subscription_key: asset.subscription_key,
|
||
sound_prompt: asset.sound_prompt,
|
||
background_music_title: asset.background_music_title,
|
||
background_music_style: asset.background_music_style,
|
||
background_music_prompt: asset.background_music_prompt,
|
||
background_music: asset.background_music,
|
||
click_sound: asset.click_sound,
|
||
background_asset: asset
|
||
.background_asset
|
||
.map(map_match3d_background_asset_for_work),
|
||
status: asset.status,
|
||
error: asset.error,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_image_view_for_agent(
|
||
view: Match3DGeneratedItemImageView,
|
||
) -> shared_contracts::match3d_agent::Match3DGeneratedItemImageViewResponse {
|
||
shared_contracts::match3d_agent::Match3DGeneratedItemImageViewResponse {
|
||
view_id: view.view_id,
|
||
view_index: view.view_index,
|
||
image_src: view.image_src,
|
||
image_object_key: view.image_object_key,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_image_view_for_work(
|
||
view: Match3DGeneratedItemImageView,
|
||
) -> shared_contracts::match3d_works::Match3DGeneratedItemImageViewResponse {
|
||
shared_contracts::match3d_works::Match3DGeneratedItemImageViewResponse {
|
||
view_id: view.view_id,
|
||
view_index: view.view_index,
|
||
image_src: view.image_src,
|
||
image_object_key: view.image_object_key,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_image_view_from_work(
|
||
view: shared_contracts::match3d_works::Match3DGeneratedItemImageViewResponse,
|
||
) -> Match3DGeneratedItemImageView {
|
||
Match3DGeneratedItemImageView {
|
||
view_id: view.view_id,
|
||
view_index: view.view_index,
|
||
image_src: view.image_src,
|
||
image_object_key: view.image_object_key,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_background_asset_for_agent(
|
||
asset: Match3DGeneratedBackgroundAsset,
|
||
) -> shared_contracts::match3d_agent::Match3DGeneratedBackgroundAssetResponse {
|
||
shared_contracts::match3d_agent::Match3DGeneratedBackgroundAssetResponse {
|
||
prompt: asset.prompt,
|
||
image_src: asset.image_src,
|
||
image_object_key: asset.image_object_key,
|
||
container_prompt: asset.container_prompt,
|
||
container_image_src: asset.container_image_src,
|
||
container_image_object_key: asset.container_image_object_key,
|
||
status: asset.status,
|
||
error: asset.error,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_background_asset_for_work(
|
||
asset: Match3DGeneratedBackgroundAsset,
|
||
) -> shared_contracts::match3d_works::Match3DGeneratedBackgroundAssetResponse {
|
||
shared_contracts::match3d_works::Match3DGeneratedBackgroundAssetResponse {
|
||
prompt: asset.prompt,
|
||
image_src: asset.image_src,
|
||
image_object_key: asset.image_object_key,
|
||
container_prompt: asset.container_prompt,
|
||
container_image_src: asset.container_image_src,
|
||
container_image_object_key: asset.container_image_object_key,
|
||
status: asset.status,
|
||
error: asset.error,
|
||
}
|
||
}
|
||
|
||
pub(super) fn find_match3d_generated_background_asset(
|
||
assets: &[Match3DGeneratedItemAsset],
|
||
) -> Option<Match3DGeneratedBackgroundAsset> {
|
||
assets
|
||
.iter()
|
||
.find_map(|asset| asset.background_asset.clone())
|
||
}
|
||
|
||
pub(super) fn resolve_match3d_default_cover_image_src(
|
||
assets: &[Match3DGeneratedItemAsset],
|
||
) -> Option<String> {
|
||
find_match3d_generated_background_asset(assets).and_then(|asset| {
|
||
asset
|
||
.container_image_src
|
||
.as_deref()
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.map(str::to_string)
|
||
.or_else(|| {
|
||
asset
|
||
.container_image_object_key
|
||
.as_deref()
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.map(str::to_string)
|
||
})
|
||
.or_else(|| {
|
||
asset
|
||
.image_src
|
||
.as_deref()
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.map(str::to_string)
|
||
})
|
||
.or_else(|| {
|
||
asset
|
||
.image_object_key
|
||
.as_deref()
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.map(str::to_string)
|
||
})
|
||
})
|
||
}
|
||
|
||
pub(super) fn find_match3d_generated_background_asset_json(
|
||
assets: &[Match3DGeneratedItemAssetJson],
|
||
) -> Option<Match3DGeneratedBackgroundAsset> {
|
||
assets
|
||
.iter()
|
||
.find_map(|asset| asset.background_asset.clone())
|
||
}
|
||
|
||
pub(super) fn apply_match3d_background_asset_to_agent_draft(
|
||
draft: &mut Match3DResultDraftResponse,
|
||
background_asset: Option<Match3DGeneratedBackgroundAsset>,
|
||
) {
|
||
if let Some(asset) = background_asset {
|
||
draft.background_prompt = Some(asset.prompt.clone());
|
||
draft.background_image_src = asset.image_src.clone();
|
||
draft.background_image_object_key = asset.image_object_key.clone();
|
||
draft.generated_background_asset = Some(map_match3d_background_asset_for_agent(asset));
|
||
}
|
||
}
|
||
|
||
pub(super) fn build_match3d_work_profile_record_with_assets(
|
||
mut item: Match3DWorkProfileRecord,
|
||
assets: &[Match3DGeneratedItemAsset],
|
||
) -> Match3DWorkProfileRecord {
|
||
item.generated_item_assets_json = serialize_match3d_generated_item_assets(assets);
|
||
if let Some(background_asset) = find_match3d_generated_background_asset(assets) {
|
||
item.cover_image_src = item.cover_image_src.or_else(|| {
|
||
background_asset
|
||
.container_image_src
|
||
.clone()
|
||
.or(background_asset.container_image_object_key.clone())
|
||
.or(background_asset.image_src.clone())
|
||
.or(background_asset.image_object_key.clone())
|
||
});
|
||
}
|
||
item
|
||
}
|
||
|
||
fn match3d_text_present(value: Option<&String>) -> bool {
|
||
value.is_some_and(|value| !value.trim().is_empty())
|
||
}
|
||
|
||
fn match3d_item_asset_has_image(asset: &Match3DGeneratedItemAssetJson) -> bool {
|
||
match3d_text_present(asset.image_src.as_ref())
|
||
|| match3d_text_present(asset.image_object_key.as_ref())
|
||
|| asset.image_views.iter().any(|view| {
|
||
match3d_text_present(view.image_src.as_ref())
|
||
|| match3d_text_present(view.image_object_key.as_ref())
|
||
})
|
||
}
|
||
|
||
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.container_image_src.as_ref())
|
||
|| match3d_text_present(asset.container_image_object_key.as_ref())
|
||
}
|
||
|
||
fn resolve_match3d_work_generation_status(
|
||
item: &Match3DWorkProfileRecord,
|
||
assets: &[Match3DGeneratedItemAssetJson],
|
||
background_asset: Option<&Match3DGeneratedBackgroundAsset>,
|
||
) -> Option<String> {
|
||
if item.publication_status.eq_ignore_ascii_case("published") {
|
||
return Some("ready".to_string());
|
||
}
|
||
|
||
if assets.is_empty()
|
||
|| !assets.iter().any(match3d_item_asset_has_image)
|
||
|| !background_asset.is_some_and(match3d_background_asset_has_image)
|
||
{
|
||
return Some("generating".to_string());
|
||
}
|
||
|
||
Some("ready".to_string())
|
||
}
|
||
|
||
pub(super) fn map_match3d_message_response(
|
||
message: Match3DAgentMessageRecord,
|
||
) -> Match3DAgentMessageResponse {
|
||
Match3DAgentMessageResponse {
|
||
id: message.message_id,
|
||
role: message.role,
|
||
kind: message.kind,
|
||
text: message.text,
|
||
created_at: message.created_at,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_work_summary_response(
|
||
item: Match3DWorkProfileRecord,
|
||
) -> Match3DWorkSummaryResponse {
|
||
let generated_item_asset_json =
|
||
parse_match3d_generated_item_assets(item.generated_item_assets_json.as_deref());
|
||
let background_asset = find_match3d_generated_background_asset_json(&generated_item_asset_json);
|
||
let generation_status = resolve_match3d_work_generation_status(
|
||
&item,
|
||
&generated_item_asset_json,
|
||
background_asset.as_ref(),
|
||
);
|
||
let generated_background_asset = background_asset
|
||
.clone()
|
||
.map(map_match3d_background_asset_for_work);
|
||
let generated_item_assets = generated_item_asset_json
|
||
.into_iter()
|
||
.map(map_match3d_generated_item_asset_for_work)
|
||
.collect();
|
||
Match3DWorkSummaryResponse {
|
||
work_id: item.work_id,
|
||
profile_id: item.profile_id,
|
||
owner_user_id: item.owner_user_id,
|
||
source_session_id: item.source_session_id,
|
||
game_name: item.game_name,
|
||
theme_text: item.theme_text,
|
||
summary: item.summary,
|
||
tags: item.tags,
|
||
cover_image_src: item.cover_image_src,
|
||
reference_image_src: item.reference_image_src,
|
||
clear_count: item.clear_count,
|
||
difficulty: item.difficulty,
|
||
publication_status: item.publication_status,
|
||
play_count: item.play_count,
|
||
updated_at: item.updated_at,
|
||
published_at: item.published_at,
|
||
publish_ready: item.publish_ready,
|
||
generation_status,
|
||
background_prompt: background_asset.as_ref().map(|asset| asset.prompt.clone()),
|
||
background_image_src: background_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.image_src.clone()),
|
||
background_image_object_key: background_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.image_object_key.clone()),
|
||
generated_background_asset,
|
||
generated_item_assets,
|
||
}
|
||
}
|
||
|
||
pub(super) fn match3d_bad_gateway(message: impl Into<String>) -> AppError {
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": MATCH3D_AGENT_PROVIDER,
|
||
"message": message.into(),
|
||
}))
|
||
}
|
||
|
||
pub(super) fn map_match3d_work_profile_response(
|
||
item: Match3DWorkProfileRecord,
|
||
) -> Match3DWorkProfileResponse {
|
||
Match3DWorkProfileResponse {
|
||
summary: map_match3d_work_summary_response(item),
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_run_response(run: Match3DRunRecord) -> Match3DRunSnapshotResponse {
|
||
Match3DRunSnapshotResponse {
|
||
run_id: run.run_id,
|
||
profile_id: run.profile_id,
|
||
owner_user_id: run.owner_user_id,
|
||
status: normalize_match3d_run_status(run.status.as_str()).to_string(),
|
||
snapshot_version: run.snapshot_version,
|
||
started_at_ms: run.started_at_ms,
|
||
duration_limit_ms: run.duration_limit_ms,
|
||
server_now_ms: run.server_now_ms,
|
||
remaining_ms: run.remaining_ms,
|
||
clear_count: run.clear_count,
|
||
total_item_count: run.total_item_count,
|
||
cleared_item_count: run.cleared_item_count,
|
||
items: run
|
||
.items
|
||
.into_iter()
|
||
.map(map_match3d_item_response)
|
||
.collect(),
|
||
tray_slots: run
|
||
.tray_slots
|
||
.into_iter()
|
||
.map(map_match3d_tray_slot_response)
|
||
.collect(),
|
||
failure_reason: run
|
||
.failure_reason
|
||
.map(|reason| normalize_match3d_failure_reason(reason.as_str()).to_string()),
|
||
last_confirmed_action_id: run.last_confirmed_action_id,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_item_response(
|
||
item: Match3DItemSnapshotRecord,
|
||
) -> Match3DItemSnapshotResponse {
|
||
Match3DItemSnapshotResponse {
|
||
item_instance_id: item.item_instance_id,
|
||
item_type_id: item.item_type_id,
|
||
visual_key: item.visual_key,
|
||
x: item.x,
|
||
y: item.y,
|
||
radius: item.radius,
|
||
layer: item.layer,
|
||
state: normalize_match3d_item_state(item.state.as_str()).to_string(),
|
||
clickable: item.clickable,
|
||
tray_slot_index: item.tray_slot_index,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_tray_slot_response(
|
||
slot: Match3DTraySlotRecord,
|
||
) -> Match3DTraySlotResponse {
|
||
Match3DTraySlotResponse {
|
||
slot_index: slot.slot_index,
|
||
item_instance_id: slot.item_instance_id,
|
||
item_type_id: slot.item_type_id,
|
||
visual_key: slot.visual_key,
|
||
}
|
||
}
|
||
|
||
pub(super) fn map_match3d_click_confirmation_response(
|
||
confirmation: Match3DClickConfirmationRecord,
|
||
) -> Match3DClickConfirmationResponse {
|
||
Match3DClickConfirmationResponse {
|
||
accepted: confirmation.accepted,
|
||
reject_reason: confirmation
|
||
.reject_reason
|
||
.map(|reason| normalize_match3d_click_reject_reason(reason.as_str()).to_string()),
|
||
entered_slot_index: confirmation.entered_slot_index,
|
||
cleared_item_instance_ids: confirmation.cleared_item_instance_ids,
|
||
run: map_match3d_run_response(confirmation.run),
|
||
}
|
||
}
|